<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Dr. Yaroslav Zhbankov on Medium]]></title>
        <description><![CDATA[Stories by Dr. Yaroslav Zhbankov on Medium]]></description>
        <link>https://medium.com/@yaroslavzhbankov?source=rss-88b741f7b069------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*sEn6bcfR3g0X_Ad_hQnfDg.png</url>
            <title>Stories by Dr. Yaroslav Zhbankov on Medium</title>
            <link>https://medium.com/@yaroslavzhbankov?source=rss-88b741f7b069------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Wed, 27 May 2026 00:57:00 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@yaroslavzhbankov/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Prompt-Driven Development as a Software Engineering Process]]></title>
            <link>https://medium.com/@yaroslavzhbankov/prompt-driven-development-as-a-software-engineering-process-a3fecc396bc4?source=rss-88b741f7b069------2</link>
            <guid isPermaLink="false">https://medium.com/p/a3fecc396bc4</guid>
            <category><![CDATA[prompt-engineering]]></category>
            <category><![CDATA[design]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Dr. Yaroslav Zhbankov]]></dc:creator>
            <pubDate>Wed, 13 May 2026 03:11:55 GMT</pubDate>
            <atom:updated>2026-05-13T03:11:55.410Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rcgo841i2p7RuDR8PMM16A.png" /></figure><p>Most “I used AI to build an app” stories are accounts of <strong>vibe coding, </strong>a fragile process where success depends on the model’s mood and the user’s luck. Vibe coding doesn’t scale because it lacks a feedback loop.</p><p><strong>The core thesis: AI-driven development only becomes a reliable engineering process when automated verification gates are inserted between every generation step.</strong></p><p>To move beyond one-shot prompts, we must treat AI interaction as an <strong>AI Compiler Pipeline</strong>. In this model, the human provides the high-level intent, and the AI acts as a multi-stage transformer moving through <strong>Specification</strong>, <strong>Verification</strong>, and <strong>Execution</strong>.</p><h3>The Problem</h3><p>I correct text constantly. Grammar checks, tone adjustments, translations. Every time: open browser, navigate to ChatGPT/Gemini, type the prompt, paste the text, copy the result. Dozens of times per day.</p><p>I wanted a tool that reduces this to: hotkey → paste → Enter → done. A floating overlay, like Spotlight, that lives in the menu bar and calls an AI API directly.</p><h3>The AI Compiler Pipeline: Spec → Verify → Execute</h3><p>The core principle is simple: <strong>Never go from idea to code in one step.</strong> Instead, treat the LLM as a series of specialized agents operating within a pipeline of <strong>Prompt Contracts</strong>.</p><ul><li><strong>Phase 1: Specification (Design).</strong> Produce a design document. Subject it to a verification gate for gaps and risks.</li><li><strong>Phase 2: Verification (Prompt Generation).</strong> Generate atomic, executable instructions. Subject them to a verification gate for logic and dependencies.</li><li><strong>Phase 3: Execution (Agentic Build).</strong> Feed validated prompts to a builder agent. Use automated checkpoints to gate progress.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TmglmJOE5pAotirJZVR71g.png" /></figure><h3>Phase 1: <strong>Specification (Design)</strong></h3><p>I started a conversation with Claude about what I wanted. We discussed technology choices, UX patterns, and constraints. The first design wasn’t the one I built from — each analysis pass caught decisions that would have become bugs.</p><p>I initially wanted double-Shift as the hotkey (like IntelliJ). Analysis revealed three layers of risk: a native binding package that breaks during Electron packaging, mandatory Accessibility permissions, and false positives when typing consecutive capitals. We switched to Cmd+Shift+G — zero dependencies, zero risk. The initial design hid the overlay on blur; analysis caught that users would click away to copy text and lose the overlay. We changed to Escape-only dismissal.</p><p>By the time I moved to prompt generation, the architecture was validated against real usage patterns, not assumptions.</p><h3>Phase 2: <strong>Verification (Prompt Generation)</strong></h3><p>When you ask an AI to “build the whole app”, the model’s focus degrades as the context window fills with boilerplate. By breaking the build into a pipeline of <strong>Prompt Contracts</strong>, you ensure the AI agent always has 100% focus on a 5% problem.</p><p>A bug caught in the <strong>Specification phase</strong> costs one sentence to fix. The same bug caught during <strong>Execution</strong> costs a full debugging session. We use <strong>Verification Gates</strong> to catch errors at their cheapest point.</p><h4>Requirements for Prompts</h4><p>Before generating any prompts, I established rules:</p><p><strong>Each prompt produces one testable feature.</strong> Settings is one prompt. API calls are another. Never both. The agent focuses better with a single objective, and if something breaks, you know exactly which prompt caused it.</p><p><strong>Each prompt includes a checkpoint.</strong> Automated checks (TypeScript compilation, grep for expected code patterns) and manual tests (press this key, expect that result, verify this edge case).</p><p><strong>Each prompt is self-contained.</strong> The agent should be able to execute it without referencing other prompts. All file paths, function signatures, CSS values, API headers are explicit.</p><p><strong>Negative constraints are explicit.</strong> “Do NOT hide on blur.” “Do NOT call app.hide() if the settings window is open”. These prevent the agent from applying common patterns that would break the specific UX.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_1yd_sJ1ucHtkqDOX3YQ2Q.png" /></figure><h4>Analysing the Prompts</h4><p>The first set of prompts looked reasonable. Then I asked Claude to analyse them as if it were the agent executing them — and each pass caught 3–5 issues that would have caused failures during execution.</p><p>Missing dependency installs that the agent might not catch on its own. An incorrect Accessibility permissions check for an API that doesn’t require it. A vague tray icon instruction (“use any default icon”) that would error because there is no default. A DMG maker package not installed by default with the template — meaning pnpm run make would fail at the very end of the build. A window resize instruction that didn&#39;t specify the IPC round-trip between renderer and main process.</p><p>After three analysis passes, the prompts were tight enough to execute.</p><h4>The Checkpoint Design</h4><p>The checkpoints aren’t an afterthought — they’re the mechanism that makes the entire approach work. Without them, the agent declares “done” when the code compiles, which is a very different thing from “working”.</p><p>A good checkpoint has three layers:</p><p><strong>Structural checks</strong> verify the code exists and is correct:</p><pre>npx tsc --noEmit                              # zero TypeScript errors<br>grep -n &quot;globalShortcut.register&quot; src/main.ts  # registration code exists<br>grep -n &quot;x-api-key&quot; src/main.ts                # API header present<br>grep -rn &quot;: any&quot; src/                          # no lazy type annotations</pre><p><strong>Functional tests</strong> verify behaviour:</p><pre>Cmd+Shift+G → overlay appears, input is focused<br>Type text → Enter → &quot;Thinking...&quot; → corrected result appears<br>Escape → overlay hides, focus returns to previous app</pre><p><strong>Edge case tests</strong> verify robustness:</p><pre>Empty API key → warning placeholder, input disabled<br>Invalid API key → red error message with 401 status<br>No network → timeout error after 30 seconds<br>Escape during loading → does nothing (prevents closing mid-request)</pre><p>The agent runs all three layers before proceeding. If any check fails, it fixes the issue and re-runs. This is what prevents the compounding errors that sink single-prompt approaches — where a small mistake in step 3 becomes an unfixable mess by step 7.</p><h3>Phase 3: <strong>Execution (Agentic Build)</strong></h3><p>With validated prompts in hand, I fed them one at a time to Claude Code. Nine prompts built the core:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1l2bUqwJwULO72AbmXX5ZQ.png" /></figure><p>Most prompts executed cleanly on the first pass. When they didn’t, the feedback loop kicked in — and this loop is a core part of the methodology, not an exception to it. The prompts can be found on GitHub <a href="https://github.com/yzhbankov/quick-prompt/tree/master/prompts">here</a>.</p><h3>Extending the Application</h3><p>After the core was working, additional prompts extended it:<br><strong>Multi-provider support</strong> added Anthropic, OpenAI, and local server (Apfel, Ollama, LM Studio) integration. This was too complex for one prompt, so I split it following the principle of data → logic → UI: first the settings schema with migration, then the API call functions for all three providers, then the settings UI with provider-switching tabs. Each prompt was independently testable.<br><strong>MacOS Services integration</strong> added a right-click context menu option in any app. Select text → right-click → “Check with Quick Prompt” → overlay opens with the text. This required an Automator workflow that writes selected text to a trigger file, and a file watcher in the Electron app. Two mechanisms, one prompt, extensive checkpoints.</p><p>Each extension followed the same three-phase pattern: design → generate prompt with checkpoints → execute and verify.</p><h3>The Result</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FVz-rb8QqEjzvloLyP1VWw.png" /><figcaption>Fig. 1. Quick Prompt application console</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1016/1*HVi7cvJugtwID_PFz2yVXw.png" /><figcaption>Fig. 2. Quick Prompt application settings window</figcaption></figure><p>One evening of work produced a distributable .dmg. The application opens with Cmd+Shift+G from any app including fullscreen, auto-pastes clipboard text, sends it to Anthropic, OpenAI, or any local AI server, shows corrected text, copies it to clipboard, and works via right-click context menu through macOS Services.</p><h3>Conclusion: The Philosophical Shift</h3><p>The future of software engineering isn’t “talking to machines”; it’s <strong>designing pipelines that verify machines.</strong></p><p>Prompt-Driven Development shifts the engineer’s role from a Writer to an <strong>Architect and Auditor</strong>. By implementing staged generation and strict verification gates, we turn the unpredictable “magic” of LLMs into a deterministic engineering process. The <strong>AI Compiler Pipeline</strong> doesn’t just write code faster — it writes code you can actually trust.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a3fecc396bc4" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Streaming Essentials: Types, Architectures, and Best Practices]]></title>
            <link>https://medium.com/@yaroslavzhbankov/streaming-essentials-types-architectures-and-best-practices-47137df8b9e3?source=rss-88b741f7b069------2</link>
            <guid isPermaLink="false">https://medium.com/p/47137df8b9e3</guid>
            <category><![CDATA[messaging]]></category>
            <category><![CDATA[cloud]]></category>
            <category><![CDATA[kafka]]></category>
            <category><![CDATA[streaming]]></category>
            <category><![CDATA[design-systems]]></category>
            <dc:creator><![CDATA[Dr. Yaroslav Zhbankov]]></dc:creator>
            <pubDate>Tue, 31 Mar 2026 03:37:34 GMT</pubDate>
            <atom:updated>2026-03-31T03:37:34.455Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ScukM7WOnI3GgdBwqtSIPA.png" /></figure><h3>Introduction</h3><p>Streaming is a foundational technique in modern computing that enables continuous, real-time processing and delivery of data. Rather than waiting for an entire dataset to be available before acting on it, streaming allows systems to process data incrementally, as it arrives. From video playback and financial market feeds to real-time analytics and event-driven microservices, streaming powers a vast range of applications that demand low latency and high throughput.</p><p>As systems grow in scale and users expect instant feedback, understanding streaming — its types, architectures, trade-offs, and technologies — becomes essential for any software engineer or system designer.</p><p>This article making an overview to the different types of streaming, processing models, architectures, technologies, use cases, and trade-offs.</p><h3>What is Streaming?</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*u8DycWWXoJhoTZZmbXItwQ.png" /></figure><p>Streaming is a method of transmitting or processing data continuously in a sequence of small, manageable chunks (often called events, records, or frames) rather than as a single, complete batch. The data is produced, transmitted, and consumed incrementally — often in real time or near real time.</p><p>There are two primary domains where streaming is applied:</p><ul><li><strong>Data Streaming</strong>: Continuous ingestion and processing of data records/events (e.g., sensor readings, clickstream events, log entries).</li><li><strong>Media Streaming</strong>: Continuous delivery of audio or video content to end users (e.g., Netflix, Spotify, YouTube).</li></ul><p>Both share the same core principle: deliver data incrementally as it becomes available, rather than waiting for the entire payload to be ready.</p><h3>Streaming Types</h3><h4>Data Streaming</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NZX84YRC7QQL6DXnOGkbLw.png" /></figure><p>Data streaming involves the continuous flow of data records from producers to consumers, typically through a message broker or streaming platform.</p><p><strong>Event Streaming</strong>: Captures events as they happen and makes them available to consumers. Events are typically immutable, ordered, and durable. Example: Apache Kafka, Amazon Kinesis, Azure Event Hubs.</p><p><strong>Message Streaming</strong>: Delivers messages between producers and consumers through a message broker. Messages can be point-to-point or publish-subscribe. Example: RabbitMQ, Amazon SQS, Apache ActiveMQ.</p><p><strong>Log Streaming</strong>: Aggregates and streams log data from multiple sources for monitoring and analysis. Example: Fluentd, Logstash, AWS CloudWatch Logs.</p><p><strong>Change Data Capture (CDC) Streaming</strong>: Captures row-level changes (INSERT, UPDATE, DELETE) from a database and streams them to downstream systems. Example: Debezium, AWS DMS, Oracle GoldenGate.</p><p><strong>IoT Data Streaming</strong>: Continuous data flow from sensors, devices, and edge nodes. Often involves high-volume, low-latency requirements. Example: AWS IoT Core, Azure IoT Hub, MQTT brokers.</p><h4>Media Streaming</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GVz6f02e5DTJ1t3cvsXfcA.png" /></figure><p>Media streaming involves the continuous delivery of audio, video, or other multimedia content over a network.</p><p><strong>Video Streaming</strong>: Delivers video content progressively to viewers. Can be live (real-time broadcast) or on-demand (pre-recorded). Example: Netflix (on-demand), Twitch (live), YouTube (both).</p><p><strong>Audio Streaming</strong>: Delivers audio content continuously to listeners. Example: Spotify, Apple Music, internet radio stations.</p><p><strong>Live Streaming</strong>: Real-time broadcast of audio/video content with minimal delay. Example: Twitch, YouTube Live, Facebook Live.</p><p><strong>Adaptive Bitrate Streaming (ABR)</strong>: Dynamically adjusts video quality based on network conditions and device capabilities. Example: HLS (HTTP Live Streaming), DASH (Dynamic Adaptive Streaming over HTTP).</p><h4>API and Application Streaming</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ePGenS_W8P3TbzB7Sklocg.png" /></figure><p><strong>Server-Sent Events (SSE)</strong>: A server pushes updates to the client over a single HTTP connection. Unidirectional (server to client). Example: Real-time dashboards, notification feeds.</p><p><strong>WebSocket Streaming</strong>: Full-duplex, bidirectional communication over a single TCP connection. Example: Chat applications, multiplayer games, collaborative editing.</p><p><strong>gRPC Streaming</strong>: Supports unary, server-streaming, client-streaming, and bidirectional streaming RPCs over HTTP/2. Example: Microservice communication, real-time telemetry.</p><p><strong>HTTP Chunked Transfer</strong>: Server sends response in chunks without knowing the total size in advance. Example: Large file downloads, streaming API responses (e.g., LLM token streaming).</p><p><strong>HTTP/2 Server Push and Streaming</strong>: Multiplexed streams over a single connection enabling concurrent requests and server-initiated resource push. Example: API streaming responses, multiplexed microservice communication.</p><p><strong>HTTP/3 / QUIC</strong>: HTTP over QUIC (UDP-based) eliminates head-of-line blocking with independent per-stream flow control and built-in encryption. Example: Low-latency web streaming, mobile streaming over unreliable networks.</p><p><strong>Socket.IO</strong>: Library on top of WebSockets adding auto-reconnect, rooms, broadcasting, and HTTP long-polling fallback. Example: Chat applications, real-time collaboration.</p><p><strong>Mercure</strong>: Open protocol built on SSE for real-time push with JWT authorization and topic-based subscriptions. Example: Real-time updates for REST API-based applications.</p><p><strong>Long Polling</strong>: Client sends HTTP request; server holds it open until data is available. Simulates server push. Example: Legacy real-time applications, restricted network environments.</p><h4>Reactive and Runtime-Level Streaming</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*q4nkZHK8EjR-lYTEL0C2Bg.png" /></figure><p><strong>Reactive Streams (Java Flow API)</strong>: Standard specification for async stream processing with non-blocking backpressure. Example: Interoperability between reactive libraries.</p><p><strong>Project Reactor</strong>: Reactive Streams implementation (`Mono`/`Flux`) powering Spring WebFlux. Example: Non-blocking Spring microservices.</p><p><strong>RxJava / RxJS / ReactiveX</strong>: Cross-platform reactive programming with Observables and rich operators. Example: UI event composition, async data source merging.</p><p><strong>Akka Streams / Apache Pekko Streams</strong>: Graph-based reactive stream processing on the actor model with built-in backpressure. Example: Complex JVM data transformation pipelines.</p><p><strong>Node.js Streams</strong>: Built-in Readable/Writable/Transform streams with backpressure via `pipe()`. Example: File processing, HTTP streaming in Node.js.</p><p><strong>Spring Cloud Stream</strong>: Framework for event-driven microservices with binder abstractions for Kafka, RabbitMQ, Kinesis. Example: Spring Boot services with broker-agnostic messaging.</p><p><strong>Apache Camel</strong>: Integration framework with 300+ connectors implementing Enterprise Integration Patterns. Example: Enterprise system integration with streaming modes.</p><h3>Stream Processing Models</h3><h4>Real-Time (Event-at-a-Time) Processing</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Gq78IXEzmSeA5nJGIW-3cA.png" /></figure><p>Each event is processed individually as it arrives. Provides the lowest latency but requires careful state management.</p><p><strong>Pros</strong>: Lowest latency; immediate reaction to events.<br><strong>Cons</strong>: Complex state management; harder to achieve exactly-once semantics.<br><strong>Use Case</strong>: Fraud detection, real-time alerting, stock trading systems.</p><h4>Micro-Batch Processing</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8SkKhbP_zsSYpq5jKAjjbA.png" /></figure><p>Events are collected into small batches (e.g., every 1–5 seconds) and processed together. A middle ground between true streaming and batch.</p><p><strong>Pros</strong>: Simpler programming model; better throughput; easier fault tolerance.<br><strong>Cons</strong>: Higher latency than event-at-a-time; not truly real-time.<br><strong>Use Case</strong>: Near-real-time analytics, Spark Structured Streaming workloads.</p><h4>Batch Processing</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7m8xpKCiKBs-cqNtBv5Ayw.png" /></figure><p>Processes a bounded, complete dataset at once. Not streaming, but often compared against it.</p><p><strong>Pros</strong>: Simplest model; high throughput; well-understood semantics.<br><strong>Cons</strong>: High latency (minutes to hours); stale results.<br><strong>Use Case</strong>: ETL pipelines, daily reporting, historical analysis.</p><h4>Lambda Architecture</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1EEsdSNhXh_PCMSQ5vmKvQ.png" /></figure><p>Combines batch and stream processing layers. A batch layer processes historical data for accuracy; a speed layer processes real-time data for low latency. Results are merged at query time.</p><p><strong>Pros</strong>: Handles both historical and real-time data; fault-tolerant.<br><strong>Cons</strong>: Dual codebase to maintain; operational complexity.<br><strong>Use Case</strong>: Systems needing both real-time and retroactive analytics.</p><h4>Kappa Architecture</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*43opyQLTjDTLLm3e7sMHQQ.png" /></figure><p>Simplifies Lambda by using a single stream processing layer for both real-time and historical data. All data is treated as a stream; reprocessing is done by replaying the stream.</p><p><strong>Pros</strong>: Single codebase; simpler ops; stream-native.<br><strong>Cons</strong>: Requires a replayable log (e.g., Kafka); reprocessing can be expensive.<br><strong>Use Case</strong>: Event-sourced systems, modern real-time analytics.</p><h3>Streaming Architectures</h3><h4>Publish-Subscribe (Pub/Sub)</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1seJ3YJfeP-nh6KMIXZUtQ.png" /></figure><p>Producers publish messages to a topic; consumers subscribe to topics of interest. Decouples producers from consumers.</p><p><strong>Key Properties</strong>: Decoupled communication; fan-out to multiple consumers; topic-based routing.<br><strong>Technologies</strong>: Apache Kafka, Google Pub/Sub, Amazon SNS, Redis Pub/Sub.<br><strong>Use Case</strong>: Event-driven microservices, notification systems.</p><h4>Message Queue</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pGuhBT-mfkPsz8jKjZndBg.png" /></figure><p>Producers enqueue messages; consumers dequeue and process them. Typically point-to-point with competing consumers.</p><p><strong>Key Properties</strong>: Load balancing across consumers; guaranteed delivery; ordering within partitions.<br><strong>Technologies</strong>: RabbitMQ, Amazon SQS, Apache ActiveMQ, Azure Service Bus.<br><strong>Use Case</strong>: Task distribution, workload balancing, asynchronous processing.</p><h4>Event Log / Commit Log</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fbCoMvyr9LP-cRjmTxElMg.png" /></figure><p>An append-only, ordered, durable log of events. Consumers read from the log at their own pace using offsets.</p><p><strong>Key Properties</strong>: Durable; replayable; ordered; supports multiple consumer groups.<br><strong>Technologies</strong>: Apache Kafka, Apache Pulsar, Amazon Kinesis, Redpanda.<br><strong>Use Case</strong>: Event sourcing, CDC, audit logging, data integration.</p><h4>Stream Processing Pipeline</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*npG8QNnO7zTVQ9AF93FZsg.png" /></figure><p>A topology of processing stages (sources, operators, sinks) connected by streams. Supports transformations, aggregations, windowing, and joins.</p><p><strong>Key Properties</strong>: Stateful processing; windowed aggregations; exactly-once semantics (in some engines).<br><strong>Technologies</strong>: Apache Flink, Apache Spark Structured Streaming, Apache Storm, Kafka Streams.<br><strong>Use Case</strong>: Real-time analytics, complex event processing, ETL.</p><h4>Materialized View Pattern</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Xc_7CjLzGh1jbE2moUFH0w.png" /></figure><p>Stream processors continuously update a queryable “materialized view” (table or index) as events arrive. Consumers query the view rather than the raw stream.</p><p><strong>Key Properties</strong>: Low-latency reads; updated incrementally; derived from source events.<br><strong>Technologies</strong>: Kafka Streams (KTable), Apache Flink (queryable state), ksqlDB.<br><strong>Use Case</strong>: Real-time dashboards, serving layer for analytics, CQRS read models.</p><h3>Delivery Guarantees</h3><p>One of the most critical aspects of streaming systems is how they handle message delivery in the presence of failures.</p><h4>At-Most-Once</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NqpEZ5eU8192sFOxSKmqiQ.png" /></figure><p>Messages may be lost but are never delivered more than once. The producer sends and forgets; no retries.</p><p><strong>Pros</strong>: Simplest; lowest latency; no duplicates.<br><strong>Cons</strong>: Data loss is possible.<br><strong>Use Case</strong>: Metrics collection where occasional loss is acceptable (e.g., telemetry).</p><h4>At-Least-Once</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*z75ALUQxugax0abpszcQWA.png" /></figure><p>Messages are guaranteed to be delivered but may be delivered more than once. The producer retries on failure.</p><p><strong>Pros</strong>: No data loss.<br><strong>Cons</strong>: Duplicate processing possible; consumers must be idempotent.<br><strong>Use Case</strong>: Most streaming applications where data loss is unacceptable.</p><h4>Exactly-Once</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TZ0G91ukaey-u_jftw5Smw.png" /></figure><p>Each message is processed exactly once, even in the presence of failures. Achieved via transactions, deduplication, or idempotent writes.</p><p><strong>Pros</strong>: Strongest guarantee; simplifies application logic.<br><strong>Cons</strong>: Higher latency and overhead; complex to implement.<br><strong>Use Case</strong>: Financial transactions, billing, inventory management.</p><h3>Windowing Strategies</h3><p>When processing unbounded streams, windowing defines how events are grouped for aggregation.</p><h4>Tumbling Window</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*x0w1Tasia994bwzQ9-G9Jg.png" /></figure><p>Fixed-size, non-overlapping windows. Each event belongs to exactly one window.</p><p><strong>Example</strong>: Count events every 5 minutes → [0–5min], [5–10min], [10–15min].<br><strong>Use Case</strong>: Periodic aggregations, regular reporting intervals.</p><h4>Sliding Window</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xoKrXXJeC6jM1VokBnhBaQ.png" /></figure><p>Fixed-size windows that slide by a configurable interval. Windows overlap, so an event may belong to multiple windows.</p><p><strong>Example</strong>: 10-minute window sliding every 1 minute.<br><strong>Use Case</strong>: Moving averages, trend detection.</p><h4>Session Window</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6RHnY0yhMfmggX396kNcjQ.png" /></figure><p>Dynamic windows that group events by activity. A window closes after a configurable gap of inactivity.</p><p><strong>Example</strong>: Group user clickstream events with a 30-minute inactivity timeout.<br><strong>Use Case</strong>: User session analysis, activity tracking.</p><h4>Global Window</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yy-cPMlJVlV0ZjPnh7NXxw.png" /></figure><p>A single window that encompasses all events. Requires a custom trigger to emit results.</p><p><strong>Use Case</strong>: Accumulating state across the entire stream.</p><h3>Backpressure and Flow Control</h3><p>When consumers cannot keep up with producers, streaming systems need mechanisms to handle the imbalance.</p><p><strong>Buffering</strong>: Queue messages in a buffer (in-memory or on-disk). <strong>Pros</strong>: Smooths out temporary spikes. <strong>Cons</strong>: Buffer overflow if sustained.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a-ukkcmPisMnIAuRUUrnJw.png" /></figure><p><strong>Dropping</strong>: Discard messages when the system is overwhelmed. <strong>Pros</strong>: Protects system stability. <strong>Cons</strong>: Data loss.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JxL1C0YOBhqSqpyYo4izZQ.png" /></figure><p><strong>Rate Limiting</strong>: Throttle producers to match consumer throughput. <strong>Pros</strong>: Prevents overload. <strong>Cons</strong>: Adds latency on the producer side.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LEYtIUK9dTQpbO80Ut5S0A.png" /></figure><p><strong>Backpressure Propagation</strong>: Signal upstream components to slow down. This is the reactive streams approach. <strong>Pros</strong>: End-to-end flow control. <strong>Cons</strong>: Requires protocol support.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*m74Ex57EWbbQqXZImiAZQg.png" /></figure><p><strong>Technologies</strong>: Reactive Streams (Java), Akka Streams, Apache Flink (credit-based flow control), TCP flow control.</p><h3>Batch vs Stream Processing — Key Differences</h3><p><strong>Data scope</strong> — Batch operates on a bounded, finite dataset known upfront. Stream operates on an unbounded, continuous flow with no defined end.</p><p><strong>Latency</strong> — Batch produces results after minutes to hours because it waits for the full dataset to be collected before processing begins. Stream produces results in milliseconds to seconds, processing each event as it arrives.</p><p><strong>Throughput</strong> — Batch achieves very high throughput through optimized bulk I/O operations. Stream has high throughput but carries per-event overhead that batch avoids.</p><p><strong>Complexity</strong> — Batch is simpler — the map/reduce model is well-understood. Stream is significantly more complex because you must handle out-of-order events, late data, distributed state, and partial failures mid-flight.</p><p><strong>Fault tolerance</strong> — If a batch job fails, you restart and reprocess the whole batch. If a stream job fails, you resume from the last checkpoint or Kafka offset, avoiding full reprocessing.</p><p><strong>Use cases</strong> — Batch is the right tool for ETL pipelines, overnight reporting, ML model training, and billing runs. Stream is the right tool for real-time dashboards, fraud detection, alerting, change data capture (CDC), and operational monitoring.</p><p><strong>Freshness</strong> — Batch data is stale by definition — it reflects the world as of the last run, which may be hours ago. Stream data is near real-time, typically seconds behind the source.</p><p><strong>State management</strong> — In batch, state is implicit: the full dataset is available in memory or on disk, so joins and aggregations are straightforward. In stream, state is explicit: you maintain managed state stores (like RocksDB in Flink or Kafka Streams) with TTLs, watermarks, and eviction policies to handle the fact that you can never see the full picture at once.</p><p><strong>Resource usage</strong> — Batch consumption is bursty: heavy CPU and memory during the batch window, then idle. Stream consumption is steady: a flat, continuous resource profile around the clock.</p><h3>Streaming Technologies Deep Dive</h3><p>This section walks through streaming technologies from the most fundamental transport-level primitives to sophisticated distributed platforms. Understanding the full spectrum helps you choose the right tool for your specific latency, throughput, durability, and complexity requirements.</p><h4>TCP Streaming</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oUodMcjXzgGAPf-Pj01thQ.png" /></figure><p><strong>How it works</strong>: TCP (Transmission Control Protocol) provides a reliable, ordered, byte-stream connection between two endpoints. A server binds to a port, accepts connections, and data flows as a continuous stream of bytes in both directions. TCP handles retransmission of lost packets, flow control (sliding window), and congestion control automatically at the OS kernel level.</p><p><strong>Key characteristics</strong>:<br>- Reliable, ordered delivery guaranteed by the protocol<br>- Connection-oriented (requires a handshake before data flows)<br>- Built-in flow control and congestion control<br>- Byte-stream abstraction (no message boundaries)</p><p><strong>Limitations</strong>:<br>- Point-to-point only — no native multicast or pub/sub<br>- Head-of-line blocking: a single lost packet stalls the entire stream until retransmitted<br>- Connection overhead: the three-way handshake adds latency for short-lived connections<br>- No built-in message framing — applications must define their own protocol for message boundaries<br>- Does not scale beyond a single connection without application-level coordination</p><p><strong>When to use</strong>: Custom low-level streaming protocols, simple producer-consumer pairs on a local network, building blocks for higher-level protocols. Use when you need reliable delivery between two known endpoints and are willing to handle framing and routing yourself.</p><h4>UDP Streaming</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fj75ThCAgP1a2ff0sRobRA.png" /></figure><p><strong>How it works</strong>: UDP (User Datagram Protocol) sends discrete packets (datagrams) between endpoints without establishing a connection. Each datagram is independent — there is no guarantee of delivery, ordering, or duplicate protection. The sender simply fires packets at a destination address and port.</p><p><strong>Key characteristics</strong>:<br>- Connectionless — no handshake required<br>- Datagram-based with clear message boundaries<br>- Supports multicast and broadcast<br>- Minimal protocol overhead (8-byte header vs. 20+ for TCP)</p><p><strong>Limitations</strong>:<br>- No delivery guarantee — packets can be lost, duplicated, or arrive out of order<br>- No built-in flow control or congestion control — the application must handle these<br>- Maximum datagram size limited by MTU (typically ~1,472 bytes on Ethernet before fragmentation)<br>- No built-in reliability — applications must implement their own ACK/retransmit logic if needed</p><p><strong>When to use</strong>: Real-time media streaming (audio/video) where low latency matters more than perfect delivery, online gaming, DNS queries, IoT telemetry where occasional data loss is acceptable, and as a foundation for protocols like WebRTC, SRT, and QUIC that add selective reliability on top of UDP.</p><h4>Unix Domain Sockets and Named Pipes</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*x_0ejgfbxL71X_pbEUgxtA.png" /></figure><p><strong>How it works</strong>: Unix domain sockets provide inter-process communication (IPC) on the same host using the filesystem namespace instead of network addresses. They support both stream (SOCK_STREAM, like TCP) and datagram (SOCK_DGRAM, like UDP) modes. Named pipes (FIFOs) provide a simpler unidirectional byte-stream between processes via a filesystem path.</p><p><strong>Key characteristics</strong>:<br>- Extremely low latency — no network stack overhead<br>- Higher throughput than TCP loopback (no TCP/IP header processing, checksumming, or routing)<br>- File-permission-based access control<br>- Stream or datagram semantics available (domain sockets)</p><p><strong>Limitations</strong>:<br>- Single-host only — cannot communicate across a network<br>- No built-in pub/sub, routing, or load balancing<br>- Named pipes are unidirectional (need two for bidirectional communication)<br>- Not portable to all operating systems in the same way (Windows uses a different mechanism)</p><p><strong>When to use</strong>: High-performance IPC on a single machine — e.g., a web server communicating with a local application server, container sidecar proxies, database client connections (PostgreSQL, MySQL, Redis all support Unix sockets for local connections).</p><h4>MQTT (Message Queuing Telemetry Transport)</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LAWUya34ZV_iKMJOgqPIgQ.png" /></figure><p><strong>How it works</strong>: MQTT is a lightweight publish-subscribe messaging protocol designed for constrained devices and unreliable networks. Clients connect to a central broker, subscribe to topic filters, and publish messages to topics. The broker routes messages from publishers to all matching subscribers. MQTT supports three Quality of Service (QoS) levels: 0 (at most once), 1 (at least once), and 2 (exactly once).</p><p><strong>Key characteristics</strong>:<br>- Extremely lightweight — minimal packet overhead (as low as 2 bytes header)<br>- Publish-subscribe with hierarchical topic structure and wildcard subscriptions<br>- Persistent sessions and retained messages<br>- Last Will and Testament (LWT) for detecting client disconnections<br>- Runs over TCP (or WebSockets for browser clients)</p><p><strong>Limitations</strong>:<br>- Centralized broker is a single point of failure (unless clustered)<br>- Not designed for high-throughput data pipelines (no native partitioning or consumer groups)<br>- Limited message size (protocol allows up to 256 MB, but practical limits are much lower)<br>- No built-in message replay or stream history<br>- QoS 2 (exactly once) adds significant latency overhead</p><p><strong>When to use:</strong> IoT and edge computing scenarios with constrained devices, low-bandwidth networks, or unreliable connectivity. Home automation, industrial telemetry, fleet tracking, mobile push notifications. Use when devices are resource-constrained and the message volume per device is moderate.</p><h4>ZeroMQ (ZMQ)</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Na_fAM27KBS3IbDRhrlG2g.png" /></figure><p><strong>How it works</strong>: ZeroMQ is a high-performance asynchronous messaging library that provides socket-like abstractions for various messaging patterns. Unlike traditional brokers, ZMQ is brokerless — it is embedded directly in applications as a library. It provides several socket types that implement specific patterns: REQ/REP (request-reply), PUB/SUB (publish-subscribe), PUSH/PULL (pipeline/fan-out), DEALER/ROUTER (async request-reply), and PAIR (exclusive pair). ZMQ handles connection management, framing, reconnection, and buffering internally.</p><p><strong>Key characteristics</strong>:<br>- Brokerless architecture — no central server required (peer-to-peer)<br>- Multiple transport protocols: TCP, IPC (Unix sockets), inproc (in-process threads), PGM/EPGM (multicast)<br>- Automatic reconnection and message queuing<br>- Multi-part messages with atomic delivery<br>- Extremely low latency (microseconds for inproc, sub-millisecond for TCP)<br>- Language bindings for 40+ languages</p><p><strong>Limitations</strong>:<br>- No message persistence or durability — messages are lost if the receiver is down and the sender’s buffer overflows<br>- No built-in message broker — you must design your own routing topology<br>- No built-in authentication or encryption (though CurveZMQ exists as an add-on)<br>- Debugging distributed ZMQ topologies can be challenging<br>- No consumer groups, offsets, or replay capability<br>- PUB/SUB has a “slow subscriber” problem — slow consumers miss messages</p><p><strong>When to use</strong>: Low-latency inter-process or inter-service communication where you do not need durability or replay. High-frequency trading systems, scientific computing pipelines, distributed task distribution, real-time data collection from multiple sources. Use when you want messaging patterns without the operational overhead of a broker.</p><h4>Redis Pub/Sub</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JnqhuYWkRT9Sck9tpHd-rA.png" /></figure><p><strong>How it works</strong>: Redis Pub/Sub provides a simple publish-subscribe messaging system built into Redis. Publishers send messages to channels; subscribers listen on channels. Messages are delivered to all connected subscribers in real time. It is a fire-and-forget system — messages are not persisted and are only delivered to subscribers connected at the time of publication.</p><p><strong>Key characteristics</strong>:<br>- Extremely simple API (PUBLISH, SUBSCRIBE, PSUBSCRIBE for pattern matching)<br>- Very low latency (sub-millisecond on local network)<br>- Pattern-based subscriptions with glob-style matching<br>- No message persistence — pure real-time delivery<br>- Part of Redis, so no additional infrastructure if Redis is already in use</p><p><strong>Limitations</strong>:<br>- No message persistence — if a subscriber is disconnected, it misses all messages<br>- No consumer groups or load balancing across consumers<br>- No delivery guarantees — at-most-once only<br>- No message acknowledgment or retry mechanism<br>- A slow subscriber can cause memory buildup in the Redis output buffer, potentially crashing Redis<br>- Messages are not stored — no replay or history</p><p><strong>When to use</strong>: Real-time notifications, cache invalidation broadcasts, lightweight event signaling between services that are always online. Use when you already have Redis and need simple, ephemeral pub/sub without durability requirements.</p><h4>Redis Streams</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lbhe9YgS9YPvT2RjeLc1bQ.png" /></figure><p><strong>How it works</strong>: Redis Streams (introduced in Redis 5.0) provide a durable, append-only log data structure within Redis. Producers append entries (field-value pairs) to a stream using XADD. Each entry gets a unique, time-based ID. Consumers can read entries sequentially (XREAD), or form consumer groups (XREADGROUP) where entries are distributed among group members with acknowledgment tracking (XACK). Unacknowledged entries can be claimed by other consumers (XCLAIM) for failure recovery.</p><p><strong>Key characteristics</strong>:<br>- Durable, append-only log persisted to disk (with Redis persistence — RDB/AOF)<br>- Consumer groups with automatic load balancing and message acknowledgment<br>- Unique time-based IDs for each entry<br>- Supports blocking reads for efficient polling<br>- Pending Entry List (PEL) tracks unacknowledged messages per consumer<br>- Capped streams (MAXLEN/MINID) for automatic trimming<br>- Fan-out: multiple consumer groups can independently read the same stream</p><p><strong>Limitations</strong>:<br>- Single-node throughput limited by Redis’s single-threaded event loop<br>- No built-in partitioning across multiple Redis instances (Redis Cluster can shard, but each stream lives on one node)<br>- Memory-bound — large streams consume significant RAM (even with persistence, data is in memory)<br>- No native exactly-once semantics — at-least-once with manual deduplication<br>- Less mature ecosystem compared to Kafka for complex stream processing (no windowing, joins, etc.)<br>- No built-in schema registry or data governance</p><p><strong>When to use</strong>: Lightweight event streaming, task queues, and activity feeds when you already use Redis and need durability and consumer groups without deploying Kafka. Suitable for moderate throughput (tens of thousands of messages/second per stream) scenarios like order processing, notification pipelines, or microservice event buses.</p><h4>RabbitMQ</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vgFOH0zRdM3cgc5yoKvZvw.png" /></figure><p><strong>How it works</strong>: RabbitMQ is a traditional message broker implementing the AMQP (Advanced Message Queuing Protocol) standard. Producers publish messages to exchanges, which route messages to queues based on bindings and routing keys. Consumers subscribe to queues. RabbitMQ supports multiple exchange types: direct (exact routing key match), fanout (broadcast to all bound queues), topic (pattern-based routing), and headers (route by message headers). Messages can be persistent (written to disk) or transient.</p><p><strong>Key characteristics</strong>:<br>- Flexible routing via exchanges, bindings, and routing keys<br>- Multiple protocols: AMQP 0–9–1, AMQP 1.0, MQTT, STOMP, and HTTP<br>- Message acknowledgment with manual or automatic ACK<br>- Dead letter exchanges for failed message handling<br>- Priority queues<br>- Plugin ecosystem (management UI, federation, shovel)<br>- Quorum queues for high availability and data safety<br>- RabbitMQ Streams plugin for log-like append-only semantics</p><p><strong>Limitations</strong>:<br>- Not designed for high-throughput event streaming (optimized for smart routing, not raw throughput)<br>- Messages are deleted once consumed and acknowledged (not a replayable log, unless using Streams)<br>- Performance degrades when queues grow very large (millions of messages)<br>- Complex routing topologies can be hard to debug and maintain<br>- No native partitioning for horizontal scaling of a single queue (though consistent hash exchange helps)<br>- Clustering can be operationally complex; network partitions require careful handling</p><p><strong>When to use</strong>: Task distribution and work queues, request-reply patterns, complex message routing scenarios, systems requiring multiple protocol support. Best for traditional messaging use cases where you need flexible routing, per-message acknowledgment, and moderate throughput (thousands to low tens of thousands messages/second per queue).</p><h4>NATS and NATS JetStream</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uQPKStwIYJ055o261nESig.png" /></figure><p><strong>How it works</strong>: NATS is a lightweight, high-performance messaging system. Core NATS provides a simple pub/sub and request-reply system with at-most-once delivery — no persistence, no acknowledgment. NATS JetStream (built into the NATS server since v2.2) adds persistence, at-least-once/exactly-once delivery, consumer groups (called “push” and “pull” consumers), message replay, and stream retention policies. JetStream stores messages in streams and allows consumers to read from any position.</p><p><strong>Key characteristics</strong>:<br>- Core NATS: Extremely fast (millions of messages/second), simple pub/sub, subject-based addressing with wildcards<br>- JetStream: Durable streams, consumer acknowledgment, replay from any position, key-value and object stores<br>- Subject-based addressing (hierarchical subjects with `&gt;` and `*` wildcards)<br>- Built-in clustering and multi-tenancy (accounts)<br>- Leaf nodes for edge computing and hub-spoke topologies<br>- Single binary, minimal configuration, easy to deploy<br>- Request-reply pattern built into the protocol</p><p><strong>Limitations</strong>:<br>- Core NATS: No persistence or delivery guarantees (at-most-once only)<br>- JetStream: Younger ecosystem than Kafka; fewer integrations and connectors<br>- No built-in schema registry<br>- Stream processing capabilities are basic compared to Kafka Streams or Flink<br>- Large-scale JetStream deployments are less battle-tested than Kafka at extreme scale<br>- Limited windowing and aggregation support — primarily a messaging/streaming transport, not a processing engine</p><p><strong>When to use</strong>: Microservice communication (especially request-reply), cloud-native applications, edge computing, IoT. Core NATS for ultra-low-latency fire-and-forget messaging. JetStream when you need persistence and replay without the operational weight of Kafka. Excellent for systems that need both traditional messaging patterns and streaming in a single lightweight platform.</p><h4>Apache Kafka</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*C7sGihJarbYPh51_HhWtsA.png" /></figure><p><strong>How it works</strong>: Kafka is a distributed, partitioned, replicated commit log. Producers write records to topics, which are divided into partitions. Each partition is an ordered, immutable, append-only log. Partitions are distributed across brokers and replicated for fault tolerance. Consumers read from partitions using offsets and form consumer groups for parallel consumption — each partition is assigned to exactly one consumer within a group. Kafka retains messages for a configurable retention period (or indefinitely with log compaction).</p><p><strong>Key characteristics</strong>:<br>- Extremely high throughput (millions of messages/second per cluster)<br>- Durable, replayable commit log with configurable retention<br>- Partitioned for horizontal scalability<br>- Consumer groups with automatic partition assignment and rebalancing<br>- Exactly-once semantics via idempotent producers and transactional writes<br>- Log compaction for maintaining latest-value-per-key<br>- Rich ecosystem: Kafka Connect (connectors), Kafka Streams (processing), ksqlDB (SQL), Schema Registry<br>- KRaft mode (no ZooKeeper dependency since Kafka 3.3+)</p><p><strong>Limitations</strong>:<br>- Operational complexity — managing brokers, partitions, replication, and rebalancing<br>- Partition count changes require careful planning (rebalancing, data redistribution)<br>- Not ideal for very low-latency messaging (typical latency is low milliseconds, not microseconds)<br>- Consumer rebalancing can cause temporary processing pauses<br>- JVM-based — significant memory and resource requirements<br>- Ordering guaranteed only within a partition, not globally<br>- Not designed for point-to-point or request-reply patterns (though possible with extra work)</p><p><strong>When to use</strong>: The default choice for large-scale event streaming, data pipelines, event sourcing, CDC, log aggregation, and event-driven architectures. Use when you need a durable, high-throughput, replayable event log with a mature ecosystem. Best for scenarios with high data volumes and multiple downstream consumers.</p><h4>Apache Pulsar</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UQB-bCX3Kqcrwf5-vVXECg.png" /></figure><p><strong>How it works</strong>: Pulsar is a multi-tenant, distributed messaging and streaming platform that separates serving (brokers) from storage (Apache BookKeeper). Topics are divided into partitions, and each partition is stored as a distributed ledger in BookKeeper. This separation allows independent scaling of compute and storage. Pulsar supports both streaming (with consumer offsets and replay) and traditional queueing (shared subscription) semantics on the same topic. It includes built-in geo-replication for multi-datacenter deployments.</p><p><strong>Key characteristics</strong>:<br>- Separation of compute and storage — independent scaling<br>- Multi-tenancy with namespace isolation<br>- Built-in geo-replication across datacenters<br>- Multiple subscription modes: exclusive, shared (competing consumers), failover, key-shared<br>- Tiered storage — offload old data to S3/GCS/HDFS automatically<br>- Pulsar Functions for lightweight serverless stream processing<br>- Schema registry built in<br>- Topic compaction (similar to Kafka log compaction)</p><p><strong>Limitations</strong>:<br>- More complex architecture (brokers + BookKeeper + ZooKeeper/metadata store)<br>- Smaller community and ecosystem compared to Kafka<br>- Fewer third-party connectors and integrations<br>- BookKeeper adds operational overhead<br>- Higher learning curve due to additional concepts (tenants, namespaces, bundles)<br>- Some features (transactions, key-shared subscriptions) are newer and less battle-tested</p><p><strong>When to use</strong>: Multi-tenant platforms, multi-region deployments requiring geo-replication, use cases that need both queuing and streaming on the same infrastructure, scenarios requiring independent storage and compute scaling. Consider when you need tiered storage for cost-effective long-term retention.</p><h4>Amazon Kinesis Data Streams</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vIT_rpPx2_p_r4JLaNSyaQ.png" /></figure><p><strong>How it works</strong>: Kinesis is AWS’s fully managed real-time data streaming service. Data is organized into streams, which are composed of shards. Each shard provides a fixed capacity (1 MB/sec input, 2 MB/sec output, 1,000 records/sec input). Producers write records with a partition key; Kinesis maps the key to a shard. Consumers read from shards using the Kinesis Client Library (KCL), which handles shard assignment, checkpointing, and failover. Kinesis retains data for 24 hours by default (up to 365 days).</p><p><strong>Key characteristics</strong>:<br>- Fully managed — no servers to provision or manage<br>- Automatic shard splitting and merging for scaling<br>- Integrated with AWS ecosystem (Lambda, Firehose, Analytics, S3)<br>- Enhanced fan-out for dedicated throughput per consumer<br>- Server-side encryption at rest<br>- On-demand capacity mode (auto-scaling)</p><p><strong>Limitations</strong>:<br>- AWS-only — strong vendor lock-in<br>- Shard-level throughput limits can require careful capacity planning<br>- Higher latency than self-managed Kafka (typically 200ms+)<br>- More expensive at high scale compared to self-managed alternatives<br>- Limited to 7-day retention by default (365-day max costs significantly more)<br>- Fewer processing frameworks compared to Kafka’s ecosystem<br>- Record size limited to 1 MB</p><p><strong>When to use</strong>: AWS-native real-time data ingestion when you want zero operational overhead. Best for teams already invested in the AWS ecosystem that need managed streaming without Kafka expertise. Ideal for moderate-scale streaming with tight AWS service integration (Lambda triggers, S3 delivery via Firehose).</p><h4>QUIC</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DFZi34Lelcw6Ndc3DAdr9g.png" /></figure><p><strong>How it works</strong>: QUIC (Quick UDP Internet Connections) is a transport protocol originally developed by Google and now standardized as RFC 9000. It runs on top of UDP and provides reliable, multiplexed, encrypted connections. Unlike TCP, QUIC supports multiple independent streams within a single connection — a lost packet in one stream does not block others (no head-of-line blocking). QUIC integrates TLS 1.3 encryption directly into the transport layer and supports 0-RTT connection establishment for previously visited servers. HTTP/3 is built on QUIC.</p><p><strong>Key characteristics</strong>:<br>- Multiplexed streams without head-of-line blocking<br>- Built-in TLS 1.3 encryption (always encrypted)<br>- 0-RTT connection establishment for returning connections<br>- Connection migration — survives network changes (e.g., Wi-Fi to cellular)<br>- Improved congestion control and loss recovery per stream<br>- User-space implementation (not kernel) — faster iteration on protocol improvements</p><p><strong>Limitations</strong>:<br>- Higher CPU usage than TCP (user-space processing, encryption overhead)<br>- UDP may be throttled or blocked by some corporate firewalls and middleboxes<br>- Newer protocol — less tooling for debugging and monitoring compared to TCP<br>- Not all CDNs, load balancers, and proxies fully support QUIC yet<br>- Implementation complexity is higher than TCP</p><p><strong>When to use</strong>: Modern web streaming (HTTP/3), mobile applications where connection migration matters, any scenario where head-of-line blocking in TCP is a bottleneck (multiple concurrent streams), low-latency CDN delivery.</p><h4>Google Cloud Pub/Sub</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*urV0AD-W_tlNigIGhIlS4Q.png" /></figure><p><strong>How it works</strong>: Google Cloud Pub/Sub is a fully managed, serverless messaging service. Publishers send messages to topics; the service stores them durably and delivers them to subscriptions. Each subscription receives an independent copy of every message (fan-out). Subscribers can pull messages on demand or configure push delivery to an HTTPS endpoint. Messages are retained for up to 31 days (configurable). Ordering is available via ordering keys. Supports exactly-once delivery within Cloud Dataflow pipelines.</p><p><strong>Key characteristics</strong>:<br>- Fully managed, globally distributed — no capacity planning<br>- At-least-once delivery with message ordering via ordering keys<br>- Push and pull subscription modes<br>- Dead-letter topics for undeliverable messages<br>- Message filtering at the subscription level (attribute-based)<br>- Schema validation (Avro, Protocol Buffers)<br>- Seek — replay messages from a timestamp or snapshot</p><p><strong>Limitations</strong>:<br>- GCP-only — strong vendor lock-in<br>- Message size limited to 10 MB<br>- Ordering guaranteed only within the same ordering key (not globally)<br>- No native stream processing — requires Dataflow/Beam for processing<br>- Higher latency than self-managed Kafka (~100ms typical)<br>- Cost can escalate with very high throughput</p><p><strong>When to use</strong>: GCP-native event ingestion and microservice communication. Best for teams on GCP wanting zero-ops messaging. Ideal for event-driven architectures, data pipeline ingestion into BigQuery/Dataflow, and workloads that benefit from global distribution.</p><h4>Azure Event Hubs</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uxvLe_o5iOEI4-4Sc3w55Q.png" /></figure><p><strong>How it works</strong>: Azure Event Hubs is a fully managed, big data streaming platform on Azure. Producers send events to event hubs (analogous to Kafka topics), which are partitioned for parallelism. Consumer groups enable multiple independent readers. Event Hubs provides a Kafka-compatible API endpoint — existing Kafka producers and consumers can connect with only configuration changes (no code changes). Event Hubs Capture automatically delivers events to Azure Blob Storage or Azure Data Lake for long-term retention.</p><p><strong>Key characteristics</strong>:<br>- Fully managed with automatic scaling (throughput units or Processing Units in Premium/Dedicated)<br>- Apache Kafka protocol compatibility (Kafka producer/consumer APIs work directly)<br>- Event Hubs Capture for automatic archival to Blob Storage/Data Lake<br>- Partitioned consumer model with consumer groups<br>- AMQP 1.0, Kafka, and HTTPS protocols<br>- Up to 90-day retention (7 days default)<br>- Schema Registry built in</p><p><strong>Limitations</strong>:<br>- Azure-only — vendor lock-in<br>- Throughput unit model can require careful capacity planning (Standard tier)<br>- Higher latency than self-managed Kafka for some workloads<br>- Limited stream processing built-in — requires Azure Stream Analytics or external engine<br>- Partition count cannot be changed after creation (Standard tier)<br>- Premium/Dedicated tiers needed for advanced features (significantly higher cost)</p><p><strong>When to use</strong>: Azure-native event streaming, migrating Kafka workloads to Azure with minimal code changes, IoT data ingestion (via IoT Hub routing to Event Hubs), telemetry collection. Ideal for organizations already on Azure wanting managed streaming.</p><h4>Redpanda</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vcK-NdyFPtPGgVmw3f6Sbg.png" /></figure><p><strong>How it works</strong>: Redpanda is a Kafka-compatible streaming platform written in C++ using the Seastar framework (a high-performance async framework for I/O-intensive applications). It implements the Kafka protocol so that existing Kafka clients, tools, and ecosystems (Kafka Connect, Schema Registry, etc.) work without modification. Redpanda eliminates the JVM and ZooKeeper dependencies — it runs as a single binary with an embedded Raft-based consensus protocol for metadata management.</p><p><strong>Key characteristics</strong>:<br>- Full Kafka API compatibility — drop-in replacement for Kafka<br>- No JVM — written in C++ with thread-per-core architecture (Seastar)<br>- No ZooKeeper — built-in Raft consensus<br>- Lower tail latency (p99) compared to Kafka due to no GC pauses<br>- Simpler operations — single binary, auto-tuning, fewer moving parts<br>- Built-in Schema Registry and HTTP Proxy (Pandaproxy)<br>- WebAssembly (Wasm) inline data transforms</p><p><strong>Limitations</strong>:<br>- Smaller community and ecosystem compared to Kafka<br>- Some Kafka features may lag behind the latest Kafka release<br>- Less battle-tested at extreme scale (multi-PB deployments)<br>- Commercial features (Tiered Storage, RBAC) require enterprise license<br>- Fewer managed service options compared to Kafka (Confluent, MSK, etc.)</p><p><strong>When to use</strong>: When you want Kafka compatibility with simpler operations and lower latency. Best for teams that find Kafka’s JVM tuning and ZooKeeper management burdensome. Ideal for latency-sensitive workloads, resource-constrained environments, or when operational simplicity is a priority.</p><h4>Apache ActiveMQ / ActiveMQ Artemis</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qOPrBTDi6kXiGf6V1V3iVg.png" /></figure><p><strong>How it works</strong>: Apache ActiveMQ Classic is a traditional, full-featured message broker implementing JMS (Java Message Service), AMQP, STOMP, MQTT, and OpenWire protocols. ActiveMQ Artemis is its next-generation successor — a high-performance, non-blocking architecture inspired by HornetQ. Artemis supports persistent and non-persistent messaging, queues and topics, message groups, transactions, and clustering. Both support master-slave replication for high availability.</p><p><strong>Key characteristics</strong>:<br>- Multi-protocol support: AMQP 1.0, STOMP, MQTT, OpenWire, HornetQ<br>- JMS 2.0 compliant (Java Message Service)<br>- Persistent messaging with journal-based storage (Artemis)<br>- Message groups for ordered processing per group<br>- Clustering with load balancing and HA (failover pairs)<br>- Large message support (stream large messages to disk)<br>- Diverts for routing messages between addresses</p><p><strong>Limitations</strong>:<br>- Not designed for high-throughput event streaming (not a commit log)<br>- Messages deleted after consumption (no replay without DLQ/re-routing)<br>- Clustering complexity — network of brokers can be hard to manage<br>- Classic ActiveMQ has known scalability limits; Artemis addresses many but is less widely deployed<br>- Smaller community momentum compared to Kafka/RabbitMQ in recent years<br>- No native partitioning model for horizontal scaling of a single queue</p><p><strong>When to use</strong>: JMS-based enterprise applications, Java EE environments, multi-protocol broker needs. Best for traditional enterprise messaging patterns (request-reply, point-to-point, pub/sub) where JMS compliance is required. Artemis is the preferred choice for new deployments.</p><h4>Amazon EventBridge</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0jDAAJTbA34tykpFcAsQFQ.png" /></figure><p><strong>How it works</strong>: Amazon EventBridge is a serverless event bus that makes it easy to connect applications using events. Events from AWS services (e.g., EC2 state changes, S3 uploads), SaaS integrations (e.g., Zendesk, Shopify), and custom applications are routed to targets based on rules. Rules match event patterns (JSON-based) and route matching events to targets (Lambda, SQS, SNS, Step Functions, API Gateway, etc.). EventBridge also provides Schema Registry (auto-discovers event schemas), event replay (archive and replay past events), and pipes (point-to-point integrations with filtering and enrichment).</p><p><strong>Key characteristics</strong>:<br>- Serverless — no infrastructure to manage, pay-per-event<br>- 100+ AWS service event sources and SaaS integrations built-in<br>- Content-based filtering with JSON pattern matching rules<br>- Schema Registry with automatic schema discovery<br>- Event archive and replay capability<br>- EventBridge Pipes for point-to-point integrations with enrichment<br>- Cross-account and cross-region event delivery</p><p><strong>Limitations</strong>:<br>- AWS-only — cannot be used outside AWS<br>- Higher latency than Kafka/Kinesis (~500ms typical)<br>- Limited throughput compared to Kafka (soft limits, need to request increases)<br>- Event size limited to 256 KB<br>- No stream processing capabilities — routing only<br>- Debugging complex routing rules can be challenging<br>- Not suitable for high-throughput, low-latency data streaming</p><p><strong>When to use</strong>: AWS-native event-driven architectures with serverless compute. Best for routing events between AWS services and SaaS applications. Ideal when you need content-based routing, schema management, and integration with Lambda/Step Functions without managing messaging infrastructure.</p><h4>Debezium</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VaaY3aBQCi0QnZ4LBKsTlw.png" /></figure><p><strong>How it works</strong>: Debezium is an open-source distributed platform for Change Data Capture built on top of Kafka Connect. It deploys database-specific connectors as Kafka Connect source connectors. Each connector reads the database’s transaction log (WAL in PostgreSQL, binlog in MySQL, oplog in MongoDB, etc.) and converts row-level changes into structured change events published to Kafka topics. Each table gets its own topic. Change events include before/after snapshots, operation type, source metadata, and transaction information. Debezium also performs an initial consistent snapshot of existing data before switching to streaming mode.</p><p><strong>Key characteristics</strong>:<br>- Connectors for PostgreSQL, MySQL, MongoDB, SQL Server, Oracle, Db2, Cassandra, Vitess, and Spanner<br>- Consistent initial snapshot + continuous CDC streaming<br>- Change events include before/after state, operation type, and metadata<br>- Built on Kafka Connect — leverages its scalability, offset management, and fault tolerance<br>- Embedded engine mode (use Debezium as a library without Kafka Connect)<br>- Single Message Transforms (SMTs) for event transformation<br>- Outbox pattern support for reliable event publishing from microservices</p><p><strong>Limitations</strong>:<br>- Requires Kafka (or Kafka Connect compatible system) in most deployment modes<br>- Database-specific connector maturity varies (PostgreSQL and MySQL are most mature)<br>- Initial snapshot of large databases can take hours and impact database performance<br>- Schema changes require careful handling (some connectors handle DDL better than others)<br>- Logical replication slots in PostgreSQL can cause WAL accumulation if Debezium is down<br>- Monitoring and operational tooling is basic compared to commercial CDC solutions</p><p><strong>When to use</strong>: Streaming database changes to Kafka for downstream consumers. Best for microservice data synchronization, populating search indexes (Elasticsearch), cache invalidation, building event-sourced systems from existing databases, and keeping data warehouses in sync with operational databases.</p><h4>EventStoreDB</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aA2hdh3fnr3H864L32xaLQ.png" /></figure><p><strong>How it works</strong>: EventStoreDB is a purpose-built database designed for event sourcing. Events are appended to streams (ordered sequences of events identified by a stream name). Each event has a type, data payload (JSON or binary), metadata, and a monotonically increasing position. Streams support optimistic concurrency control via expected version checks. Projections (written in JavaScript) run server-side to transform and combine events from multiple streams into new derived streams. Subscriptions (catch-up and persistent) allow consumers to process events in real-time or from a specific position.</p><p><strong>Key characteristics</strong>:<br>- Immutable, append-only event storage with per-stream ordering<br>- Optimistic concurrency control for write consistency<br>- Server-side projections for event transformation and aggregation<br>- Catch-up subscriptions (from any position) and persistent subscriptions (consumer groups)<br>- System projections: $by_category, $by_event_type, $streams for built-in event organization<br>- HTTP and gRPC client APIs<br>- Scavenge process for reclaiming space from deleted/truncated streams<br>- Cluster mode with leader election for high availability</p><p><strong>Limitations</strong>:<br>- Purpose-built for event sourcing — not a general-purpose database or message broker<br>- Smaller ecosystem compared to Kafka or PostgreSQL<br>- Server-side projections (JavaScript engine) have performance limits at high volume<br>- Operational tooling and monitoring less mature than mainstream databases<br>- Not designed for high-throughput data streaming (better for domain event volumes)<br>- Learning curve for event sourcing patterns if team is unfamiliar</p><p><strong>When to use</strong>: Domain-driven design with event sourcing, CQRS implementations, audit-critical systems, applications where the history of state changes is as important as the current state. Ideal when your primary data model is an event stream per aggregate.</p><h4>Technology Comparison Matrix</h4><p><strong>Transport and Protocols</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZnOKnzXi9Gvnqr2llPlFcQ.png" /></figure><p><strong>Message Brokers and Event Streaming Platforms</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MvUMH7vMKGkh1kzM39-WnQ.png" /></figure><p><strong>Managed Cloud Services</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_btb14wvrrGiPahZFaDLVA.png" /></figure><p><strong>Stream Processing Engines</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*i9PTrNnmcYgWzLCYFfgdxg.png" /></figure><p><strong>Media Streaming Protocols</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pr-jg5g4Q5N4CGvoH58Lcw.png" /></figure><p><strong>Client-Side / API Streaming</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bS1ZCIoHlYVXxiaEds3kzg.png" /></figure><h3>Streaming Patterns for System Design</h3><h4>Event Sourcing</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BBC3RH0VArSqGJKHLxXWgQ.png" /></figure><p>Instead of storing the current state, store an immutable sequence of events that led to the current state. The state can be reconstructed by replaying events.</p><p><strong>Benefits</strong>: Full audit trail; temporal queries; supports retroactive changes.<br><strong>Challenges</strong>: Event schema evolution; storage growth; replay performance.<br><strong>Technologies</strong>: Kafka (event log), EventStoreDB, Axon Framework.</p><h4>CQRS (Command Query Responsibility Segregation)</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*W7ieoHZD_ol-4cxT_hpdsg.png" /></figure><p>Separate the write model (commands) from the read model (queries). Stream processors update read-optimized materialized views from the write-side event stream.</p><p><strong>Benefits</strong>: Independent scaling of reads and writes; optimized read models.<br><strong>Challenges</strong>: Eventual consistency between write and read sides.<br><strong>Technologies</strong>: Kafka + Kafka Streams, Flink + Elasticsearch.</p><h4>Saga Pattern (Choreography)</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8DQsDPfeOVortzpGjDJVLw.png" /></figure><p>Coordinate distributed transactions through a sequence of events. Each service listens for events and publishes its own. Compensating events handle rollbacks.</p><p><strong>Benefits</strong>: Decoupled services; no central coordinator.<br><strong>Challenges</strong>: Complex failure handling; difficult to debug.<br><strong>Technologies</strong>: Kafka, RabbitMQ, any event broker.</p><h4>Stream-Table Duality</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dzFiwDwKz8CdAaS-Ri5aRA.png" /></figure><p>A stream can be viewed as a changelog of a table, and a table can be viewed as a snapshot of a stream at a point in time. This duality enables powerful joins and enrichments.</p><p><strong>Benefits</strong>: Enables stream-table joins; unifies batch and streaming semantics.<br><strong>Technologies</strong>: Kafka Streams (KTable/KStream), ksqlDB, Apache Flink.</p><h4>Dead Letter Queue (DLQ)</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VEKm34_3l13qXdRLzgx59A.png" /></figure><p>Messages that cannot be processed (after retries) are routed to a separate queue for investigation and reprocessing.</p><p><strong>Benefits</strong>: Prevents poison messages from blocking the pipeline; enables debugging.<br><strong>Technologies</strong>: Kafka DLQ topics, RabbitMQ dead letter exchanges, AWS SQS DLQ.</p><h3>Streaming Trade-offs</h3><h4>Latency vs. Throughput</h4><p>Lower latency (processing each event individually) reduces throughput due to per-event overhead. Micro-batching improves throughput at the cost of higher latency.</p><p><strong>Low Latency</strong>: Process events one-at-a-time for immediate results but at lower throughput.<br><strong>High Throughput</strong>: Batch multiple events together for efficiency but with added delay.</p><h4>Ordering vs. Scalability</h4><p>Strict global ordering limits parallelism. Partitioned ordering (ordering within a partition) enables horizontal scaling while maintaining order where it matters.</p><p><strong>Strict Ordering</strong>: Single partition or single consumer — limits throughput.<br><strong>Partitioned Ordering</strong>: Order per key/partition — enables parallelism with per-entity ordering.</p><h4>Consistency vs. Availability</h4><p>In distributed streaming systems, strong consistency (exactly-once, synchronized state) reduces availability during partitions. Eventual consistency improves availability.</p><p><strong>Strong Consistency</strong>: Exactly-once semantics; synchronized state; higher latency.<br><strong>Eventual Consistency</strong>: At-least-once with idempotent consumers; lower latency; temporary inconsistencies.</p><h4>Durability vs. Performance</h4><p>Persisting every event to disk ensures durability but adds I/O overhead. In-memory processing is faster but risks data loss.</p><p><strong>Durable</strong>: Write-ahead logs, replication — no data loss but slower.<br><strong>Ephemeral</strong>: In-memory only — fastest but data loss on failure.</p><h4>Complexity vs. Freshness</h4><p>Real-time streaming architectures are more complex (state management, failure handling, backpressure) than batch, but provide fresher data.</p><p><strong>Batch</strong>: Simple, well-understood, but data is stale.<br><strong>Streaming</strong>: Complex, operationally demanding, but data is fresh.</p><h4>Cost vs. Real-Time Requirements</h4><p>Streaming infrastructure (always-on clusters, partitions, replication) costs more than periodic batch jobs. Not every use case justifies the expense.</p><p><strong>Streaming</strong>: Higher infrastructure cost; justified for real-time requirements.<br><strong>Batch</strong>: Lower cost; sufficient when latency of minutes or hours is acceptable.</p><h3>When to Use Streaming?</h3><h4>Real-Time Analytics and Monitoring</h4><p>Data must be analyzed as it arrives — dashboards, metrics, anomaly detection.<br><strong>When</strong>: You need up-to-the-second insights, not stale batch reports.</p><h4>Event-Driven Architectures</h4><p>Services communicate through events rather than synchronous API calls.<br><strong>When</strong>: You are building loosely coupled microservices that react to state changes.</p><h4>Real-Time User Experiences</h4><p>Users expect instant feedback — live notifications, chat, collaborative editing, live scores.<br><strong>When</strong>: User experience degrades with even seconds of delay.</p><h4>Fraud Detection and Alerting</h4><p>Suspicious activity must be detected and acted upon immediately.<br><strong>When</strong>: Delayed detection means financial or security losses.</p><h4>IoT and Sensor Data</h4><p>Millions of devices emit continuous data streams that must be ingested, processed, and acted upon.<br><strong>When</strong>: High-volume, high-velocity data from distributed devices.</p><h4>Data Integration and CDC</h4><p>Keep multiple systems in sync by streaming changes from a source of truth.<br><strong>When</strong>: You need near-real-time replication across databases, search indexes, or caches.</p><h4>Log Aggregation and Observability</h4><p>Aggregate logs, metrics, and traces from distributed systems for real-time observability.<br><strong>When</strong>: Debugging production issues requires real-time visibility.</p><h4>Media Delivery</h4><p>Deliver audio and video content to users without requiring full downloads.<br><strong>When</strong>: Content is large, consumption is sequential, and users expect immediate playback.</p><h3>When NOT to Use Streaming?</h3><p><strong>Simple CRUD applications</strong>: A traditional request-response model suffices.<br><strong>Infrequent data updates</strong>: If data changes hourly or daily, batch processing is simpler and cheaper.<br><strong>Small data volumes</strong>: The overhead of streaming infrastructure is not justified.<br><strong>Complex ad-hoc queries</strong>: Streaming excels at predefined queries; for exploratory analysis, batch/SQL is better.<br><strong>Budget constraints</strong>: Streaming infrastructure is costlier to run and maintain than periodic batch jobs.</p><h3>Conclusion</h3><p>Streaming has evolved from a niche technique for media delivery into a foundational paradigm for building real-time, event-driven systems at scale. Whether you are processing millions of IoT events per second, powering a live dashboard, delivering video to millions of users, or keeping microservices in sync through event-driven communication, streaming provides the architecture to handle continuous, unbounded data with low latency and high throughput.</p><p>Choosing the right streaming approach requires understanding the trade-offs: latency vs. throughput, ordering vs. scalability, consistency vs. availability, and complexity vs. freshness. The best systems match their streaming strategy to their actual requirements — not every application needs exactly-once semantics or sub-millisecond latency.</p><p>In a world where users expect instant responses and businesses demand real-time insights, streaming is no longer optional — it is an essential tool in the modern system designer’s toolkit.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=47137df8b9e3" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building Vision AI Without Training: A Practical Guide to Gemini Embeddings 2]]></title>
            <link>https://medium.com/@yaroslavzhbankov/building-vision-ai-without-training-a-practical-guide-to-gemini-embeddings-2-ccdaef4b51bf?source=rss-88b741f7b069------2</link>
            <guid isPermaLink="false">https://medium.com/p/ccdaef4b51bf</guid>
            <category><![CDATA[vector-database]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[vector-embeddings]]></category>
            <category><![CDATA[computer-vision]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Dr. Yaroslav Zhbankov]]></dc:creator>
            <pubDate>Thu, 19 Mar 2026 03:14:13 GMT</pubDate>
            <atom:updated>2026-03-19T03:14:13.358Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cXxLoIqc6nEcqSBS4e302w.png" /></figure><h3>Background</h3><p>Imagine pointing a camera at a supermarket shelf and asking, <em>“Where is the rosemary?”</em> and getting an instant answer. No fine-tuned YOLO model. No labeled dataset. No training pipeline.</p><p>Or imagine a factory safety system detecting a “person lying on the floor” without a single developer ever teaching it what a “fallen person” looks like.</p><p>This is the power of <strong>Gemini Embedding 2</strong>. Google’s latest multimodal model maps images and text into a shared <a href="https://ai.google.dev/gemini-api/docs/models/gemini-embedding-2-preview"><strong>3072-dimensional vector space [1]</strong></a>. Because the math is the same for both media types, you can calculate the distance between a photo and an English phrase using simple cosine similarity.</p><p>In this guide, we’ll build two practical examples in under 50 lines of code each:</p><ol><li><strong>Visual Phrase Search:</strong> A heatmap generator to locate specific items in a busy image.</li><li><strong>Zero-Shot Alert System:</strong> A real-time monitor for custom safety anomalies.</li></ol><h3>What Are Embeddings</h3><p>Think of embeddings as coordinates in a high-dimensional space where <strong>meaning becomes distance</strong>.</p><ul><li>“rosemary” is close to images of rosemary</li><li>“fire” is close to flames and smoke</li><li>unrelated concepts (e.g., “car” and “banana”) are far apart</li></ul><p>Instead of training a classifier, you convert text → vector; convert image → vector; measure <strong>distance (similarity). </strong>If they are close → they match. This is why you can search images using natural language — without training anything.</p><h3>Why Multimodal Embeddings Change Everything</h3><p>Traditional computer vision is rigid. To detect a new object, you must collect images, label bounding boxes, retrain, and redeploy.</p><p><strong>Multimodal embeddings flip the script:</strong></p><ul><li><strong>Zero-shot detection:</strong> Describe your target in plain English.</li><li><strong>No training data:</strong> Generalizes well across many domains.</li><li><strong>Dynamic updates:</strong> Change the search query in the UI, and the “model” updates instantly.</li></ul><p>In Gemini Embeddings 2 the embedding dimension is 3072, and the cross-modal similarity range is compressed (typically between <strong>0.15 and 0.55</strong>), but within that range the signal is remarkably strong.</p><h3>Setup</h3><p>You need a Google AI Studio API key (free tier works) and three Python <a href="https://docs.cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/embedding-2">packages [2]</a>:</p><pre>pip install google-genai Pillow python-dotenv<br><br>export GOOGLE_API_KEY=your_key_here</pre><h3>Example 1: Visual Phrase Search — “Find the Rosemary”</h3><p><strong>The idea:</strong> Take a photo, split an image into a grid, embed every patch, and compare them to the search phrase. The result is a heatmap of semantic relevance.</p><p>Here is the complete, minimal implementation:</p><pre>import base64, math, os<br>from io import BytesIO<br>from PIL import Image<br>from google import genai<br>from google.genai import types<br><br>client = genai.Client(api_key=os.environ[&quot;GOOGLE_API_KEY&quot;])<br>MODEL = &quot;gemini-embedding-2-preview&quot;<br><br>def embed_image(img: Image.Image) -&gt; list[float]:<br>    &quot;&quot;&quot;Embed a PIL image using Gemini Embedding 2.&quot;&quot;&quot;<br>    buf = BytesIO()<br>    img.save(buf, format=&quot;JPEG&quot;, quality=85)<br>    b64 = base64.b64encode(buf.getvalue()).decode()<br>    resp = client.models.embed_content(<br>        model=MODEL,<br>        contents=types.Content(parts=[<br>            types.Part(inline_data=types.Blob(<br>                mime_type=&quot;image/jpeg&quot;, data=b64<br>            ))<br>        ]),<br>        config=types.EmbedContentConfig(<br>            task_type=&quot;SEMANTIC_SIMILARITY&quot;<br>        ),<br>    )<br>    return resp.embeddings[0].values<br><br>def embed_text(text: str) -&gt; list[float]:<br>    &quot;&quot;&quot;Embed a text phrase using the same model.&quot;&quot;&quot;<br>    resp = client.models.embed_content(<br>        model=MODEL,<br>        contents=text,<br>        config=types.EmbedContentConfig(<br>            task_type=&quot;SEMANTIC_SIMILARITY&quot;<br>        ),<br>    )<br>    return resp.embeddings[0].values<br><br>def cosine(a, b):<br>    dot = sum(x * y for x, y in zip(a, b))<br>    mag = math.sqrt(sum(x*x for x in a)) * math.sqrt(sum(x*x for x in b))<br>    return dot / mag if mag else 0.0<br><br>def search_in_image(image_path: str, phrase: str, grid: int = 10):<br>    &quot;&quot;&quot;Find where in an image a phrase matches best.&quot;&quot;&quot;<br>    image = Image.open(image_path).convert(&quot;RGB&quot;)<br>    w, h = image.size<br>    pw, ph = w // grid, h // grid<br>    # 1. Embed every grid patch<br>    patch_embeddings = []<br>    for r in range(grid):<br>        for c in range(grid):<br>            box = (c * pw, r * ph, (c+1) * pw, (r+1) * ph)<br>            patch = image.crop(box)<br>            patch_embeddings.append(embed_image(patch))<br>    # 2. Embed the search phrase<br>    text_emb = embed_text(phrase)<br>    # 3. Score each patch<br>    scores = [cosine(emb, text_emb) for emb in patch_embeddings]<br>    # 4. Find top matches<br>    ranked = sorted(enumerate(scores), key=lambda x: x[1], reverse=True)<br>    for idx, score in ranked[:3]:<br>        r, c = divmod(idx, grid)<br>        print(f&quot;  row={r+1} col={c+1} score={score:.4f}&quot;)<br>    return scores<br><br>search_in_image(&quot;spice_shelf.jpg&quot;, &quot;rosemary&quot;)</pre><p><strong>That is it.</strong> Under 50 lines of logic. No model training. No labeled bounding boxes.</p><p>When I ran this against a photo of a supermarket spice shelf (Fig. 1), the system correctly highlighted the rosemary section with the highest similarity score (Fig. 2) — and when I changed the phrase “ground black pepper”, the heatmap shifted to the correct area (Fig. 3).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YgQO57O9koOsCNo92tymig.png" /><figcaption>Fig. 1. Original photo of supermarket spice shelf</figcaption></figure><h4>How It Looks</h4><p>The output is a color-coded heatmap overlay: green regions have low similarity, yellow is moderate, and red is high. The top 3 matching patches get highlighted bounding boxes — red for the best match, orange for second, yellow for third.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*n8McBaz55KARH3VGc03atw.png" /><figcaption>Fig. 2. Search for “rosemary”</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ePbhdmZijLiMrC80Yckn9Q.png" /><figcaption>Fig. 3. Search for “ground black pepper”</figcaption></figure><p>The beauty is that I never told the model what “rosemary” looks like. The shared embedding space already “understands” the visual–semantic relationship.</p><h3>Example 2: Custom Manufacturing Alerts — Zero-Training Anomaly Detection</h3><p>Now let’s flip the use case. Instead of searching <em>within</em> an image, we compare an <em>entire</em> camera frame against a set of user-defined alert phrases. If the similarity score crosses a threshold, we fire an alert.</p><p>Think of it as a completely customizable surveillance system where the operator types what to watch for in plain English:</p><ul><li>“person lying on the floor”</li><li>“forklift moving near pedestrian”</li><li>“spill on the ground”</li><li>“smoke or fire”</li><li>“worker without hard hat”</li></ul><p>No retraining. No new dataset. Just a new string.</p><pre>import base64, math, os, time<br>from io import BytesIO<br>from PIL import Image<br>from google import genai<br>from google.genai import types<br><br>client = genai.Client(api_key=os.environ[&quot;GOOGLE_API_KEY&quot;])<br>MODEL = &quot;gemini-embedding-2-preview&quot;<br><br>def embed_image(img: Image.Image) -&gt; list[float]:<br>    buf = BytesIO()<br>    img.save(buf, format=&quot;JPEG&quot;, quality=85)<br>    b64 = base64.b64encode(buf.getvalue()).decode()<br>    resp = client.models.embed_content(<br>        model=MODEL,<br>        contents=types.Content(parts=[<br>            types.Part(inline_data=types.Blob(<br>                mime_type=&quot;image/jpeg&quot;, data=b64<br>            ))<br>        ]),<br>        config=types.EmbedContentConfig(<br>            task_type=&quot;SEMANTIC_SIMILARITY&quot;<br>        ),<br>    )<br>    return resp.embeddings[0].values<br><br>def embed_text(text: str) -&gt; list[float]:<br>    resp = client.models.embed_content(<br>        model=MODEL,<br>        contents=text,<br>        config=types.EmbedContentConfig(<br>            task_type=&quot;SEMANTIC_SIMILARITY&quot;<br>        ),<br>    )<br>    return resp.embeddings[0].values<br><br>def cosine(a, b):<br>    dot = sum(x * y for x, y in zip(a, b))<br>    mag = math.sqrt(sum(x*x for x in a)) * math.sqrt(sum(x*x for x in b))<br>    return dot / mag if mag else 0.0<br><br># ── User-defined alerts ─────────────────────────────────────<br>ALERTS = [<br>    &quot;person lying on the floor&quot;,<br>    &quot;forklift near a pedestrian&quot;,<br>    &quot;liquid spill on the ground&quot;,<br>    &quot;smoke or fire&quot;,<br>]<br>THRESHOLD = 0.35   # Tune based on your environment<br># Pre-embed all alert phrases once at startup<br>alert_embeddings = {phrase: embed_text(phrase) for phrase in ALERTS}<br><br>def check_frame(frame: Image.Image):<br>    &quot;&quot;&quot;Compare one camera frame against all alert phrases.&quot;&quot;&quot;<br>    frame_emb = embed_image(frame)<br>    for phrase, phrase_emb in alert_embeddings.items():<br>        score = cosine(frame_emb, phrase_emb)<br>        if score &gt;= THRESHOLD:<br>            print(f&quot;  ALERT  [{score:.3f}] {phrase}&quot;)<br><br># ── Simulated camera loop ───────────────────────────────────<br>def monitor(image_source: str, interval: int = 5):<br>    &quot;&quot;&quot;Poll a camera (or image file) and check for alerts.&quot;&quot;&quot;<br>    print(f&quot;Monitoring: {image_source}&quot;)<br>    print(f&quot;Watching for: {&#39;, &#39;.join(ALERTS)}&quot;)<br>    print(f&quot;Threshold: {THRESHOLD}\n&quot;)<br>    while True:<br>        frame = Image.open(image_source).convert(&quot;RGB&quot;)<br>        check_frame(frame)<br>        time.sleep(interval)<br><br>monitor(&quot;camera_feed.jpg&quot;)</pre><h3>Why This Is Powerful</h3><p>In traditional manufacturing vision systems, adding a new alert type means:</p><ol><li>Collecting hundreds of labeled examples</li><li>Retraining or fine-tuning a detection model</li><li>Redeploying the model</li><li>Hoping it generalizes</li></ol><p>With multimodal embeddings, a floor manager can literally type “worker without safety goggles” into a text field and the system starts monitoring for it <em>immediately</em>. The cost of a new alert is one API call to embed the new phrase.</p><h3>Score Calibration</h3><p>Cross-modal similarity with Gemini Embedding 2 operates in a compressed range (<strong>0.5+</strong> Very high match, <strong>0.42–0.5</strong> Strong match, <strong>0.33–0.42</strong> moderate match, <strong>0.22–0.33</strong> weak match, <strong>&lt; 0.22</strong> little/no match). This is due to the <strong>Modality Gap [2, 3]</strong>. While Gemini maps images and text into the same space, they tend to cluster in slightly different “neighborhoods.” To the model, a photo of a rose and the word “rose” are conceptually identical, but structurally different.</p><p>For an alert system, start with a threshold around <strong>0.35</strong> and adjust based on your false-positive tolerance. You can also use a two-tier system: <strong>0.35</strong> for “warning” and <strong>0.45</strong> for “critical.”</p><h3>Combining Both Approaches</h3><p>The two examples are complementary. In a real deployment you might:</p><ol><li><strong>First pass (Example 2):</strong> Compare the full frame against alert phrases. If “person lying on floor” scores above threshold, proceed to step 2.</li><li><strong>Second pass (Example 1):</strong> Run the grid search to <em>locate where</em> in the frame the match is strongest, drawing a bounding box around the detected area.</li></ol><p>This gives you both detection and localization — all from text descriptions, all with zero training.</p><h4>Scaling With a Vector Database</h4><p>For image-to-image search (e.g., “find all frames similar to this incident”), store embeddings in a vector database like ChromaDB:</p><pre>import chromadb<br><br>db = chromadb.PersistentClient(path=&quot;./alert_db&quot;)<br>collection = db.get_or_create_collection(<br>    name=&quot;camera_frames&quot;,<br>    metadata={&quot;hnsw:space&quot;: &quot;cosine&quot;},<br>)<br># Store a frame<br>collection.upsert(<br>    ids=[&quot;frame_001&quot;],<br>    embeddings=[frame_embedding],<br>    metadatas=[{&quot;timestamp&quot;: &quot;2025-03-15T10:30:00&quot;, &quot;camera&quot;: &quot;floor_2&quot;}],<br>)<br># Find similar past incidents<br>results = collection.query(<br>    query_embeddings=[new_frame_embedding],<br>    n_results=5,<br>)</pre><p>This enables historical search: “show me all frames that looked like this incident” — useful for audits, pattern analysis, and compliance.</p><h3>Limitations and Practical Tips</h3><p><strong>Accuracy [4].</strong> Multimodal embeddings are not a replacement for fine-tuned object detectors when you need pixel-precise bounding boxes or 99.9% recall. They are best for flexible, zero-shot, “good enough” detection where adaptability matters more than precision.</p><p><strong>Latency.</strong> Each embedding API call takes ~200–500ms. For the grid search (100 patches), that is 100 sequential API calls. Batch them, reduce grid size, or use async calls for production.</p><p><strong>Score range.</strong> Cross-modal scores are compressed (0.15–0.55). Do not compare them to text-to-text similarity. Always calibrate thresholds on your specific domain.</p><p><strong>Patch size matters.</strong> Too small and the patch loses context. Too large and you lose localization precision. A 10x10 grid is a good starting point for most images.</p><h3>Conclusion</h3><p><strong>Gemini Embedding 2</strong> makes a category of computer vision tasks trivially easy that previously required significant ML infrastructure. The ability to compare any image with any text phrase — using a single API and basic cosine similarity — opens up use cases that were simply impractical before:</p><ul><li><strong>Retail:</strong> Search product shelves by name</li><li><strong>Manufacturing:</strong> Custom safety alerts in plain English</li><li><strong>Agriculture:</strong> Identify plant species in field photos</li><li><strong>Security:</strong> Describe suspicious behavior in words</li><li><strong>Healthcare:</strong> Flag visual anomalies by description</li></ul><p>Zero training. Zero labels. Just embeddings and cosine similarity.</p><h3>References</h3><ol><li><strong>Google AI for Developers.</strong> (2026). <em>Gemini Embedding 2 Model Documentation</em>. <a href="https://ai.google.dev/gemini-api/docs/models/gemini-embedding-2-preview">ai.google.dev/gemini-api/docs/models/gemini-embedding-2-preview</a></li><li><strong>Google Cloud Documentation.</strong> (2026). <em>Gemini Embedding 2 on Vertex AI</em>. <a href="https://docs.cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/embedding-2">docs.cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/embedding-2</a></li><li><strong>Choi, M. &amp; Duerig, T.</strong> (2026). <em>Gemini Embedding 2: Our first natively multimodal embedding model</em>. Google DeepMind Blog. <a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-embedding-2/">blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-embedding-2/</a></li><li><strong>MindStudio Team.</strong> (2026). <em>What Is Matryoshka Representation Learning in Gemini Embedding 2?</em> <a href="https://www.mindstudio.ai/blog/matryoshka-representation-learning-gemini-embedding-2">mindstudio.ai/blog/matryoshka-representation-learning-gemini-embedding-2</a></li><li><strong>VentureBeat.</strong> (2026). <em>Google’s Gemini Embedding 2 arrives with native multimodal support to cut costs</em>. <a href="https://venturebeat.com/data/googles-gemini-embedding-2-arrives-with-native-multimodal-support-to-cut">venturebeat.com/data/googles-gemini-embedding-2-arrives-with-native-multimodal-support-to-cut</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ccdaef4b51bf" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Vector Databases: Searching by Meaning — The Essential Engine of the LLM Era]]></title>
            <link>https://medium.com/@yaroslavzhbankov/vector-databases-searching-by-meaning-the-essential-engine-of-the-llm-era-1982794e7542?source=rss-88b741f7b069------2</link>
            <guid isPermaLink="false">https://medium.com/p/1982794e7542</guid>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[semantic-search]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[vector-database]]></category>
            <dc:creator><![CDATA[Dr. Yaroslav Zhbankov]]></dc:creator>
            <pubDate>Wed, 25 Feb 2026 04:24:36 GMT</pubDate>
            <atom:updated>2026-02-25T04:24:36.511Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DI1AfrKcaVjHXaG995Zleg.png" /></figure><h3>Background</h3><p>In recent years, the AI revolution has fundamentally reshaped how data stores, retrieves, and reasons. Traditional relational databases — built around structured rows, columns, and exact keyword matches — were never designed to handle the kind of data that powers today’s AI applications: high-dimensional embeddings that capture the <em>meaning</em> of text, images, audio, and more. This gap gave rise to vector databases, a specialized class of data storage systems purpose-built for similarity search in high-dimensional spaces.<br>Whether you are building a chatbot grounded in proprietary documents, a semantic product search, or a recommendation engine that understands user intent — chances are a vector database sits at the core of architecture. In this article, will be explored what vector databases are, why they have become indispensable, how they work under the hood, and walk through a practical example using Python.</p><h3>What Is a Vector Database?</h3><p>A vector database is a specialized database designed to store, index, and query vector embeddings — dense numerical representations of data in high-dimensional space. Unlike traditional databases that match rows on exact values or keyword patterns, vector databases find records that are <em>semantically similar</em> to a query.</p><p>Consider a simple example. In a relational database, searching for “running shoes for wide feet” requires the exact phrase (or careful keyword matching). A vector database, on the other hand, understands that “broad-fit athletic footwear” is semantically close — because both phrases map to nearby points in embedding space.</p><h4>Embeddings: The Foundation</h4><p>An embedding is a numerical vector (an array of floating-point numbers) produced by a machine learning model. These models — such as OpenAI’s text-embedding-ada-002 or the open-source all-MiniLM-L6-v2 from Sentence Transformers — convert unstructured data into fixed-length vectors (e.g., 384, 768, or 1536 dimensions).</p><p>The key property: semantically similar inputs produce vectors that are close together in the embedding space. This transforms the problem of “understanding meaning” into a geometric problem of “finding nearby points.”</p><h4>How It Differs From Traditional Databases</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Aa90UF0MkkXEMeqa69m5Wg.png" /></figure><h3>Why Vector Databases Became So Popular</h3><p>The meteoric rise of vector databases is directly tied to the AI and LLM boom. Several converging trends created the perfect conditions.</p><h4>1. The Large Language Model Revolution</h4><p>The release of GPT-3 in 2020 and ChatGPT in late 2022 triggered an avalanche of AI-powered applications across every industry. LLMs are powerful, but they have critical limitations: they hallucinate, their training data has a knowledge cutoff, and they lack access to proprietary information. Vector databases solve these problems through Retrieval-Augmented Generation (RAG), which we will explore later.</p><h4>2. Retrieval-Augmented Generation (RAG)</h4><p>RAG has emerged as the dominant architecture for enterprise AI. Instead of fine-tuning a model (expensive, static, and slow), RAG retrieves relevant context from a vector database and injects it into the LLM prompt at inference time. This single pattern has made vector databases a requirement rather than a nice-to-have.</p><h4>3. Embedding Models Became Accessible</h4><p>Open-source embedding models — from Sentence Transformers, Cohere, and others — made it trivial to convert text, images, and code into high-quality vectors. When generating embeddings costs fractions of a cent per document, the barrier to adopting vector search collapses. At the moment the most popular solutions are:</p><ul><li><strong>OpenAI </strong><strong>text-embedding-3-large</strong>: The industry standard for production. It supports up to 3,072 dimensions but features &quot;Matryoshka&quot; technology, allowing you to shorten embeddings to 256 or 512 dimensions with minimal accuracy loss to save on database storage. Priced at approximately <strong>$0.13 per 1M tokens</strong>. Its smaller sibling, text-embedding-3-small (1,536 dimensions), comes in at just <strong>$0.02 per 1M tokens</strong>, making it one of the most cost-effective commercial options.</li><li><strong>Voyage AI </strong><strong>voyage-3-large</strong>: Frequently tops the MTEB (Massive Text Embedding Benchmark). Known for specialized variants tuned specifically for legal, medical, and code-based datasets. Pricing sits around <strong>$0.18 per 1M tokens</strong>, with the standard voyage-3 at roughly <strong>$0.06 per 1M tokens</strong>. The specialized models (e.g., voyage-law-2, voyage-code-3) fall in a similar range, offering strong domain-specific performance for the price.</li><li><strong>Cohere </strong><strong>embed-english-v3.0</strong>: Highly optimized for RAG. It is unique because it is trained to handle &quot;noisy&quot; real-world queries and provides a &quot;search_query&quot; vs &quot;search_document&quot; parameter to improve retrieval accuracy. Pricing is approximately <strong>$0.10 per 1M tokens</strong>, with a generous free tier that includes around 100M tokens per month for trial and prototyping use cases.</li><li><strong>Google </strong><strong>text-embedding-005</strong>: Integrated deeply with the Gemini ecosystem. It offers a massive context window and is particularly strong at multilingual retrieval across 100+ languages. Priced at roughly <strong>$0.00025 per 1K characters</strong> (approximately <strong>$0.01–0.04 per 1M tokens</strong> depending on average token length), making it one of the most affordable commercial embedding APIs available.</li></ul><p>Prices mentioned for the models are accurate as of February 2026.</p><h3>Where Are Vector Databases Used?</h3><h4>Retrieval-Augmented Generation (RAG)</h4><p>The most transformative use case. Organizations embed their internal documents — knowledge bases, policies, product catalogs, support tickets — into a vector database. When a user asks a question, the system:</p><ol><li>Converts the query to an embedding</li><li>Searches the vector database for the most relevant document chunks</li><li>Passes these chunks as context to the LLM</li><li>The LLM generates a grounded, accurate answer</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Tf407T06NHi1o6XdyP9Hzw.png" /></figure><p>This approach reduces hallucinations, keeps the system up-to-date without retraining, and is significantly cheaper than fine-tuning.</p><h4>Semantic Search</h4><p>Traditional full-text search matches keywords. Semantic search understands intent. A user searching for “how to fix a leaking faucet” finds results about “plumbing repair” and “dripping tap solutions” — because the meaning is similar even though the words differ. Vector databases power semantic search across e-commerce product discovery, enterprise knowledge management, legal document retrieval, and more.</p><h4>Recommendation Systems</h4><p>By representing users and items as vectors in the same embedding space, recommendation engines can surface relevant content without relying on explicit ratings. For example, on an e-commerce platform, each product is embedded based on attributes like style, fabric, color, and customer reviews. Browsing one item triggers a nearest-neighbor search to find semantically similar products.</p><h4>Image and Multimodal Search</h4><p>With multimodal models (e.g., CLIP), images and text share the same embedding space. A user can type “sunset over mountains” and retrieve matching photographs — or upload an image to find visually similar products. This enables reverse image search, visual product discovery, and content moderation at scale.</p><h4>Anomaly Detection</h4><p>Normal behavior patterns are stored as vectors. When a new event — a financial transaction, network request, or sensor reading — produces an embedding that is far from any known cluster, it is flagged as anomalous. This powers fraud detection in fintech, intrusion detection in cybersecurity, and quality control in manufacturing.</p><h3>How Vector Databases Work</h3><p>Understanding vector databases requires grasping three core concepts: distance metrics, indexing algorithms, and the query pipeline.</p><h4>Distance Metrics</h4><p>To determine how “similar” two vectors are, vector databases compute distances using one of several metrics:</p><ul><li>Cosine Similarity — Measures the angle between two vectors, ignoring magnitude. Ideal for text embeddings where direction matters more than length. Values range from -1 (opposite) to 1 (identical).</li><li>Euclidean Distance (L2) — Measures the straight-line distance between two points. Smaller values indicate higher similarity. Works well when the absolute position in space matters.</li><li>Dot Product — Computes the product of corresponding components. Sensitive to both direction and magnitude. Commonly used when vectors are normalized.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EAW-hF6XZlxffoq-Cm39iQ.png" /></figure><h4>The Brute-Force Problem</h4><p>Given a query vector, the naive approach is to compute the distance to <em>every</em> vector in the database and return the closest ones. This is an exact nearest-neighbor search — and it is prohibitively slow for large datasets. Scanning 10 million 1536-dimensional vectors for each query is not practical in production.</p><h4>Approximate Nearest Neighbor (ANN) Algorithms</h4><p>Vector databases solve this with Approximate Nearest Neighbor algorithms. These sacrifice a small amount of accuracy (typically 90–95% recall) for dramatic speed improvements. The two most widely used algorithms are HNSW and IVF.</p><p><strong>HNSW (Hierarchical Navigable Small World)</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*fgpA3hVXrfvfpMJz.png" /></figure><p>HNSW is the dominant indexing algorithm in modern vector databases. It builds a multi-layered graph structure:</p><ul><li>Layer 0 (bottom): Contains all vectors, each connected to its nearest neighbors.</li><li>Higher layers: Contain progressively fewer vectors, forming a “skip list” over the graph.</li></ul><p>Search process:</p><ul><li>The query enters at the top layer, which has very few nodes</li><li>The algorithm greedily navigates to the closest node at each layer</li><li>It drops to the next layer and continues searching with more granularity</li><li>At layer 0, it refines the search among all vectors</li></ul><p>This hierarchical approach allows the algorithm to rapidly skip irrelevant regions of the vector space, achieving O(log n) query time.</p><p>Key parameters:</p><ul><li>M — Maximum connections per node. Higher values improve recall but increase memory and index build time.</li><li>ef_construction — Search width during index building. Higher values produce a better-quality graph.</li><li>ef_search — Search width during query time. The primary tuning knob for the recall-vs-latency trade-off.</li></ul><p>HNSW offers excellent query performance and supports dynamic insertions without full re-indexing, which is why it has become the default choice in databases like Qdrant, Weaviate, and pgvector.</p><p><strong>IVF (Inverted File Index)</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GAHdDT5yHnSAhLQAyefxQQ.png" /></figure><p>IVF takes a different approach: it partitions the vector space into clusters using k-means clustering.</p><p>Search process:</p><ul><li>During indexing, vectors are assigned to their nearest cluster centroid</li><li>At query time, the algorithm identifies the closest centroids</li><li>It only searches vectors within those clusters (controlled by the nprobe parameter)</li></ul><p>Trade-offs compared to HNSW:</p><ul><li>IVF handles very large datasets efficiently because the cluster structure compresses the search space</li><li>However, IVF indexes often require full reconstruction when new data is added</li><li>HNSW generally offers better latency for real-time applications, while IVF is better suited for batch-oriented workloads</li></ul><h4>Product Quantization (PQ)</h4><p>For very large datasets where memory is a constraint, Product Quantization compresses vectors by dividing them into subvectors and replacing each with a compact code. This can reduce memory usage by 4–8x while maintaining reasonable accuracy. PQ is often combined with IVF (as IVF-PQ) for large-scale systems.</p><h3>Overview of Popular Vector Databases</h3><p>The vector database landscape is rich and rapidly evolving. Here is an overview of the most notable options, categorized by deployment model.</p><h4>Cloud-Managed / Serverless</h4><p><strong>Pinecone</strong> A fully managed, serverless vector database. Pinecone handles scaling, indexing, and infrastructure entirely, making it the easiest option to adopt. It offers excellent query performance, strong consistency, and predictable pricing. Best for teams that want zero operational overhead and are building production RAG or search applications.</p><p><strong>Zilliz Cloud</strong> The managed cloud version of the open-source Milvus. Combines Milvus’s scalability with enterprise features like auto-scaling, monitoring, and role-based access control. Ideal for organizations that want Milvus capabilities without managing the infrastructure.</p><h4>Open-Source / Self-Hosted</h4><p><strong>Qdrant</strong> Written in Rust, Qdrant is designed for high performance and production readiness. It stands out with powerful payload (metadata) filtering that integrates directly with the vector search — rather than filtering after retrieval. Offers HNSW indexing, horizontal scaling, ACID-compliant transactions, and both gRPC and REST APIs. Available as open-source (self-hosted) or as Qdrant Cloud.</p><p><strong>Weaviate</strong> Combines vector search with a knowledge graph, enabling hybrid search that blends semantic similarity with keyword matching and structured filters. Its modular architecture allows plugging in different vectorization models. Particularly strong for use cases requiring both semantic understanding and graph-based relationships.</p><p><strong>Milvus</strong> A distributed, cloud-native vector database built for billion-scale datasets. Supports multiple ANN index types (HNSW, IVF, PQ, DiskANN), GPU-accelerated search, and multi-tenancy. Backed by the Linux Foundation (LF AI &amp; Data). Best for organizations with very large datasets and complex scalability requirements.</p><p><strong>Chroma</strong> A lightweight, developer-friendly vector database designed for rapid prototyping and small-to-medium-scale AI applications. Simple API, easy to embed into Python applications, and fast to get started with. Ideal for local development and proof-of-concept projects, but may require additional infrastructure for enterprise-grade deployments.</p><h4>Database Extensions</h4><p><strong>pgvector</strong> (PostgreSQL) A PostgreSQL extension that adds vector data types, distance operations, and HNSW/IVF indexing to your existing Postgres database. The major advantage: you can combine vector search with relational queries in standard SQL — no additional infrastructure required. Realistically handles up to 10–100 million vectors before performance degrades.</p><p><strong>Redis</strong> (Vector Search) Redis Stack includes vector search capabilities, leveraging Redis’s in-memory architecture for extremely low-latency queries. A practical choice if Redis is already part of your stack and you need simple vector search without a separate database.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JqHhV6Ob4DgmHYnAZPtd7Q.png" /></figure><h3>Practical Example: Building Semantic Search With Python and Qdrant</h3><p>Let’s build a working semantic search engine. We will use Qdrant as our vector database and Sentence Transformers for generating embeddings. The scenario: a knowledge base of technical articles that we can search using natural language queries.</p><h4>Prerequisites</h4><pre>pip install qdrant-client sentence-transformers</pre><p>We will use Qdrant in in-memory mode — no Docker or cloud setup required for this example.</p><h4>Step 1: Define the Dataset</h4><pre>articles = [<br>    {<br>        &quot;id&quot;: 1,<br>        &quot;title&quot;: &quot;Introduction to Microservices Architecture&quot;,<br>        &quot;content&quot;: &quot;Microservices architecture breaks down applications into small, &quot;<br>                   &quot;independently deployable services. Each service runs in its own &quot;<br>                   &quot;process and communicates via lightweight protocols like HTTP or gRPC.&quot;,<br>        &quot;category&quot;: &quot;architecture&quot;<br>    },<br>    {<br>        &quot;id&quot;: 2,<br>        &quot;title&quot;: &quot;Understanding Container Orchestration with Kubernetes&quot;,<br>        &quot;content&quot;: &quot;Kubernetes automates the deployment, scaling, and management of &quot;<br>                   &quot;containerized applications. It groups containers into logical units &quot;<br>                   &quot;for easy management and discovery.&quot;,<br>        &quot;category&quot;: &quot;devops&quot;<br>    },<br>    {<br>        &quot;id&quot;: 3,<br>        &quot;title&quot;: &quot;Serverless Computing with AWS Lambda&quot;,<br>        &quot;content&quot;: &quot;AWS Lambda lets you run code without provisioning servers. It &quot;<br>                   &quot;scales automatically and charges only for compute time consumed, &quot;<br>                   &quot;making it ideal for event-driven workloads.&quot;,<br>        &quot;category&quot;: &quot;cloud&quot;<br>    },<br>]</pre><h4>Step 2: Initialize the Embedding Model and Qdrant Client</h4><pre>from sentence_transformers import SentenceTransformer<br>from qdrant_client import QdrantClient<br>from qdrant_client.models import Distance, VectorParams, PointStruct<br><br># Load a lightweight embedding model (384 dimensions)<br>model = SentenceTransformer(&quot;all-MiniLM-L6-v2&quot;)<br><br># Initialize Qdrant in in-memory mode (no server required)<br>client = QdrantClient(&quot;:memory:&quot;)<br><br># Create a collection<br>COLLECTION_NAME = &quot;tech_articles&quot;<br>VECTOR_SIZE = 384  # Matches the output of all-MiniLM-L6-v2<br><br>client.create_collection(<br>    collection_name=COLLECTION_NAME,<br>    vectors_config=VectorParams(<br>        size=VECTOR_SIZE,<br>        distance=Distance.COSINE,<br>    ),<br>)</pre><h4>Step 3: Generate Embeddings and Store in Qdrant</h4><pre># Prepare texts for embedding (combine title and content for richer representation)<br>texts = [f&quot;{a[&#39;title&#39;]}. {a[&#39;content&#39;]}&quot; for a in articles]<br><br># Generate embeddings in batch<br>embeddings = model.encode(texts)<br><br># Upload to Qdrant<br>points = [<br>    PointStruct(<br>        id=article[&quot;id&quot;],<br>        vector=embedding.tolist(),<br>        payload={<br>            &quot;title&quot;: article[&quot;title&quot;],<br>            &quot;content&quot;: article[&quot;content&quot;],<br>            &quot;category&quot;: article[&quot;category&quot;],<br>        },<br>    )<br>    for article, embedding in zip(articles, embeddings)<br>]<br><br>client.upsert(collection_name=COLLECTION_NAME, points=points)<br><br>print(f&quot;Indexed {len(points)} articles into Qdrant.&quot;)</pre><h4>Step 4: Search</h4><pre>def search(query: str, limit: int = 3):<br>    &quot;&quot;&quot;Search for articles semantically similar to the query.&quot;&quot;&quot;<br>    query_vector = model.encode(query).tolist()<br><br>    results = client.query_points(<br>        collection_name=COLLECTION_NAME,<br>        query=query_vector,<br>        limit=limit,<br>    )<br><br>    print(f&quot;\nQuery: \&quot;{query}\&quot;&quot;)<br>    print(&quot;-&quot; * 60)<br>    for point in results.points:<br>        print(f&quot;  Score: {point.score:.4f}&quot;)<br>        print(f&quot;  Title: {point.payload[&#39;title&#39;]}&quot;)<br>        print(f&quot;  Category: {point.payload[&#39;category&#39;]}&quot;)<br>        print()</pre><h4>Step 5: Run Queries</h4><pre># Semantic search — no exact keyword matching required<br>search(&quot;deploying containers at scale&quot;)</pre><h4>Expected Output</h4><pre>Query: &quot;deploying containers at scale&quot;<br>------------------------------------------------------------<br>  Score: 0.6376<br>  Title: Understanding Container Orchestration with Kubernetes<br>  Category: devops<br><br>  Score: 0.2931<br>  Title: Introduction to Microservices Architecture<br>  Category: architecture<br><br>  Score: 0.2482<br>  Title: Serverless Computing with AWS Lambda<br>  Category: cloud</pre><p>The vector database correctly identifies the Kubernetes article as the best match, because the meaning of the query aligns with the meaning of the article content. This is the fundamental advantage of vector search over keyword-based retrieval.</p><h3>Choosing the Right Vector Database</h3><p>There is no universal “best” vector database. The right choice depends on your constraints:</p><ul><li>Just getting started or prototyping? Start with <strong>Chroma</strong> (zero configuration) or <strong>pgvector</strong> (if you already use PostgreSQL). You can always migrate later.</li><li>Building production RAG with minimal ops? <strong>Pinecone</strong> provides a fully managed experience with strong guarantees.</li><li>Need powerful metadata filtering? <strong>Qdrant</strong> integrates filtering directly into the search algorithm rather than applying it post-retrieval.</li><li>Require hybrid semantic + keyword search? <strong>Weaviate</strong> combines both natively.</li><li>Operating at billion-vector scale? <strong>Milvus</strong> is designed for extreme scale with GPU acceleration and multiple index types.</li><li>Want to avoid new infrastructure? <strong>pgvector</strong> or <strong>Redis</strong> add vector search to databases you likely already run.</li></ul><h3>Conclusion</h3><p>Vector databases have rapidly evolved from a niche academic tool into critical infrastructure for modern AI applications. The convergence of accessible embedding models, the RAG paradigm, and the explosive growth of LLM-powered products has made vector search a fundamental building block — not a luxury.</p><p>The core idea is elegantly simple: convert data into vectors that capture meaning, then find what is similar. But the engineering behind this — HNSW graphs, quantization, distributed indexing, filtered ANN search — is what makes it work at production scale.</p><p>As the ecosystem matures, we are seeing a convergence. Traditional databases like PostgreSQL are adding vector capabilities. Dedicated vector databases are adding relational features. The line between them will continue to blur. What will not change is the underlying paradigm: searching by meaning, not by keywords.</p><p>If you are building AI-powered applications and have not yet adopted a vector database — now is the time to start. Begin with the practical example above, experiment with your own data, and evaluate which solution fits your scale and operational model.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1982794e7542" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Why AI-Driven Development Requires Strong Technical Foundations]]></title>
            <link>https://medium.com/@yaroslavzhbankov/why-ai-driven-development-requires-strong-technical-foundations-a7a85523fc05?source=rss-88b741f7b069------2</link>
            <guid isPermaLink="false">https://medium.com/p/a7a85523fc05</guid>
            <category><![CDATA[vibe-coding]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[software-architecture]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Dr. Yaroslav Zhbankov]]></dc:creator>
            <pubDate>Sun, 18 Jan 2026 02:49:54 GMT</pubDate>
            <atom:updated>2026-01-18T02:49:54.021Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JksZSXOgi-DBID8BBWiLbQ.png" /></figure><p>Everyone’s talking about AI-assisted development. I decided to go all-in: premium subscription, complex multi-service architecture, pure natural language instructions. What followed was a journey through three distinct stages — and a very different understanding of what AI can and can’t do.</p><p><strong>Spoiler: There’s no magic here. What <em>is</em> powerful is how fast AI exposes weak architecture — and how cheap it becomes once you fix it.</strong></p><h3>Stage 1: The Honeymoon Phase</h3><p>I started building a system with multiple services: an MCP server with a browser API, an AI agent, and an orchestration layer — all executing natural language instructions and performing browser actions.</p><p>The results were impressive. Claude Code asked clarifying questions, understood my intent, and generated mountains of working code from just a few high-level directives.</p><p>But here’s what nobody tells you: <strong>reading AI-generated code takes longer than you’d expect.</strong></p><p>A lot of code appears very quickly. Understanding how it all fits together? That takes real time. Getting multiple services to communicate correctly required long conversations — describing protocols, responsibilities, and communication patterns in detail.</p><p>The generated code itself was usually fine. The <em>architecture</em>, however, needed constant correction. Claude Code can follow patterns and best practices — but only if you explicitly tell it what those are. You need to describe how the system should work internally.</p><p><strong>The uncomfortable truth:</strong> You need <em>both</em> system architecture skills to direct the AI <em>and</em> deep technical skills to review the output. Vibe coding doesn’t replace either.</p><p>By the end of this stage, I had a working prototype. Not production-ready, but functional. Almost no manual coding — just reading, understanding, and giving precise instructions.</p><p>The feeling? Equal parts scary and exciting.</p><h3>Stage 2: The $60 Wake-Up Call</h3><p>Reality hit hard.</p><p>I discovered that several services in my monorepo were tightly coupled in ways that wouldn’t scale. Fair enough — I asked Claude to rework the design, and it did.</p><p>But this exposed something important: <strong>vibe coding requires constant supervision.</strong> As systems grow complex, debugging gets harder. Understanding what the AI changed gets harder. Architecture and clean abstractions aren’t optional — they’re essential.</p><p>Then came surprising moment.</p><p>I had an AI agent executing natural-language browser commands. Using a top-tier model with multiple processing loops, results were solid and reliable. The quality was genuinely good.</p><p><strong>The cost? I burned $60 in couple of minutes.</strong></p><p>A closer look showed why: massive amounts of data flowing between the agent and model to achieve that reliability. When I switched to cheaper models or reduced the data exchange, quality collapsed. Results became unpredictable and unstable.</p><p>The math became clear:</p><ul><li><strong>“Pure AI” approach:</strong> Expensive and not very reliable</li><li><strong>Cheaper approach:</strong> Requires real engineering work</li></ul><p>The initial excitement faded into something more grounded. There’s no magic. Every benefit has a cost — money, complexity, or engineering time. Pick your poison.</p><h3>Stage 3: The Engineering Breakthrough</h3><p>I was automating browser interactions through a Playwright MCP server. The server provided generic tools, and I was asking the AI to execute abstractly-described tasks using only those tools.</p><p>Results: poor, unpredictable, expensive. Still hitting that $60-per-abstract-task ceiling with premium models.</p><p>Then I tried something different.</p><p>I made the MCP tools more specific. I added an accessibility layer to the web app, effectively treating the AI like a screen reader user. Clear labels. Explicit structure. Defined actions.</p><p><strong>Everything changed.</strong></p><p>Suddenly, even cheaper models could execute reliably and repeatedly. Costs dropped from $60 to about <strong>$0.20 per run</strong> — even for complex tasks.</p><h3>The Real Lesson</h3><p>AI alone cannot solve engineering problems.</p><p>To be effective, AI needs well-designed systems, clear constraints, and engineering support. The technology is powerful, but it’s a tool — not a replacement for thinking.</p><p>This applies to vibe coding too. Simply asking AI to write code rarely solves anything meaningful. Real value comes from the engineering work: defining architecture, understanding component interactions, ensuring solutions address actual business needs.</p><p>I’ve talked to friends who are engineers at companies ranging from Amazon to small startups. The pattern is consistent: those who see AI as a productivity multiplier combine it with solid engineering fundamentals. Those who expect magic are disappointed.</p><p><strong>Three stages of discovery, condensed:</strong></p><ol><li>Vibe coding works — but you need architecture skills to direct it and technical skills to review output</li><li>“Pure AI” solutions are expensive; cheap solutions require engineering investment</li><li>The real leverage comes from building systems that help AI succeed — not from hoping AI will figure it out</li></ol><p>The world is changing. But engineering isn’t going anywhere.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a7a85523fc05" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building an MCP Server in TypeScript and Connecting with OpenAI]]></title>
            <link>https://medium.com/@yaroslavzhbankov/building-an-mcp-server-in-typescript-and-connecting-with-chatgpt-06047bfc41f8?source=rss-88b741f7b069------2</link>
            <guid isPermaLink="false">https://medium.com/p/06047bfc41f8</guid>
            <category><![CDATA[openai]]></category>
            <category><![CDATA[nodejs]]></category>
            <category><![CDATA[mcp-server]]></category>
            <category><![CDATA[typescript]]></category>
            <category><![CDATA[chatgpt]]></category>
            <dc:creator><![CDATA[Dr. Yaroslav Zhbankov]]></dc:creator>
            <pubDate>Fri, 21 Nov 2025 02:38:00 GMT</pubDate>
            <atom:updated>2025-11-26T14:34:15.874Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TYA-QZSC6oXMSBrnN7FcMw.png" /></figure><h3>Introduction</h3><p>As LLMs evolve, the demand for a standardized method to connect them with external tools, data sources, and systems has grown. While these models become excellent at reasoning and text generation, their potential is unlocked when they can interact directly with APIs, databases, and local resources, performing real-world actions beyond text. This combination creates a strong synergy effect.</p><p>The <a href="https://modelcontextprotocol.io/docs/getting-started/intro"><strong>Model Context Protocol (MCP)</strong> [1]</a> addresses this by defining a unified, structured, and secure way for LLMs to communicate with external systems. It lets developers expose tools, resources, and prompts that models can use dynamically — bridging the gap between language understanding and real-world execution.</p><p>This idea is becoming more popular because it makes integrations easier, improves reusability, and offers a clear way to extend AI systems like ChatGPT or Ollama. By the end of this article, you will have a working MCP Server connected to ChatGPT that exposes custom tools and responds to prompts in real time — providing a foundation for smarter and more context-aware AI applications.</p><h3>What will be build</h3><p>In this article, we’ll create a simple <strong>MCP Server</strong> using <strong>TypeScript</strong> and connect it to <strong>ChatGPT</strong> via <strong>OpenAI’s</strong> MCP integration. The server will expose several basic tools — such as file and database access, system health check, and email notifications — that ChatGPT can invoke to interact with external data and execute actions on your behalf.</p><p>The goal of this exercise is to demonstrate how developers can connect <strong>user-specific data sources</strong> (like databases, file systems, scripts, or APIs) and <strong>custom actions</strong> to ChatGPT through MCP. By the end, you’ll see how ChatGPT can use these tools to analyze data or trigger operations on a server — extending its capabilities beyond text reasoning.</p><p>We’ll focus on the <strong>core integration workflow</strong> and <strong>tool definition</strong>, keeping things simple and easy to follow. Topics such as <strong>scaling</strong>, <strong>authentication</strong>, and <strong>production deployment</strong> will not be covered here, as they are implementation-specific and depend on your own environment and widley described in other articles/books.</p><h3>Solution Overview</h3><p>In this article, we’ll build a simple <strong>MCP Server</strong> written in <strong>TypeScript</strong> that exposes a set of practical tools to ChatGPT via the <strong>MCP</strong>. The server will include tools for performing database queries, reading internal documentation, checking system health metrics, and even sending emails. This setup demonstrates how MCP enables seamless integration between an AI model and external systems, letting the LLM perform real-world tasks instead of only generating text.</p><p>At a high level, the solution looks like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SELbIqZA3FUwf1d_FO1q4w.png" /></figure><p>The <strong>MCP Client</strong> serves as a bridge between the LLM and the <strong>MCP Server</strong>. It converts the model’s instructions into tool calls and translates the server’s responses back into natural language. Meanwhile, the <strong>MCP Server</strong> defines and exposes tools — each representing an executable capability (e.g., system_health_check or send_email)—along with optional resources (structured data or documents that can be queried) and prompts (predefined templates or guidance for specific tasks). In the MCP context, <strong>tools</strong> perform actions, <strong>resources</strong> provide information, and <strong>prompts</strong> guide the LLM’s reasoning or formatting.</p><p>MCP supports <strong>bi-directional communication</strong> and <strong>tool discovery</strong>, meaning the client can dynamically request the server’s list of available tools and their schemas before invoking any operation. The interaction flow typically follows these steps:</p><ol><li><strong>User → MCP Client:</strong> The user sends a natural-language request (e.g., “Generate a system health check report.”).</li><li><strong>MCP Client → MCP Server:</strong> The client requests a list of available tools (tools/list).</li><li><strong>MCP Client → LLM:</strong> The client sends the user’s request and the tool list to the LLM, asking which tools to use and with what parameters.</li><li><strong>LLM → MCP Client:</strong> The LLM returns a structured plan — a list of tool calls and arguments.</li><li><strong>MCP Client → MCP Server:</strong> The client requests the execution of selected tools (tools/call), and the server runs those tools and returns the results.</li><li><strong>MCP Client → LLM:</strong> The client provides the tool results to the LLM for reasoning and final response generation.</li><li><strong>LLM → MCP Client:<br>- </strong>If the LLM needs more data, it produces another tool call → return to step 5 (loop).<br>- If the LLM has enough information, it generates the final natural-language response.</li><li><strong>MCP Client → User:</strong> The client presents the final answer to the user.</li></ol><p>This description highlights how MCP transforms the way LLMs interact with systems — making AI not only conversational but operational.</p><h3>MCP Server Implementation</h3><p>This example describes how to implement an MCP server using TypeScript. The server exposes a set of simple tools, such as running database queries, reading documentation, sending emails, and performing system health checks — just enough to experiment with the MCP.</p><p>The MCP server is implemented as a WebSocket-based server that handles requests using commands such as initialize, tools/list, and tools/call.</p><p>Below is the initialization of the server using <strong>ws</strong> package:</p><pre>import {WebSocketServer} from &#39;ws&#39;;<br>import controller from &#39;./lib/controller/index.js&#39;;<br>import {config} from &#39;./lib/config.js&#39;;<br>import {parseSafe} from &#39;./lib/utils/index.js&#39;;<br><br>const wss = new WebSocketServer({ port: config.serverPort });<br><br>console.log(`MCP WebSocket server running on ws://localhost:${config.serverPort}`);<br><br><br>wss.on(&#39;connection&#39;, (ws) =&gt; {<br>    console.log(&#39;Client connected&#39;);<br><br>    ws.on(&#39;message&#39;, async (raw) =&gt; {<br>        const msg = parseSafe(raw.toString());<br>        await controller(msg, ws);<br>    });<br><br>    ws.on(&#39;close&#39;, () =&gt; console.log(&#39;Client disconnected&#39;));<br>});</pre><p>The following code shows the controller layer implementation. Based on the request parameters, it either returns the list of available tools or executes a tool with the provided arguments:</p><pre>import {WebSocket} from &#39;ws&#39;;<br>import {tools} from &#39;../models/Tools.js&#39;;<br><br>function sendResult(client: WebSocket, id: string, result: any) {<br>    client.send(JSON.stringify({<br>        jsonrpc: &quot;2.0&quot;,<br>        id,<br>        result<br>    }));<br>}<br><br>function sendError(client: WebSocket, id: any, code: number, message: string) {<br>    client.send(JSON.stringify({<br>        jsonrpc: &quot;2.0&quot;,<br>        id,<br>        error: { code, message }<br>    }));<br>}<br><br>function handleInitialize(msg: any, client: WebSocket) {<br>    sendResult(client, msg.id, {<br>        protocolVersion: &quot;2024-11-05&quot;,<br>        serverInfo: {<br>            name: &quot;typescript-mcp-server&quot;,<br>            version: &quot;0.1.0&quot;<br>        },<br>        capabilities: {<br>            tools: {<br>                list: true,<br>                call: true<br>            }<br>        }<br>    });<br><br>    client.send(JSON.stringify({<br>        jsonrpc: &quot;2.0&quot;,<br>        method: &quot;notifications/initialized&quot;,<br>        params: {}<br>    }));<br>}<br><br>function handleToolList(msg: any, client: WebSocket) {<br>    const toolList = Array.from(tools.values()).map(t =&gt; ({<br>        name: t.name,<br>        description: t.description,<br>        inputSchema: t.inputSchema,<br>        outputSchema: t.outputSchema<br>    }));<br><br>    sendResult(client, msg.id, {<br>        tools: toolList,<br>        nextCursor: null<br>    });<br>}<br><br>async function handleCallTool(msg: any, client: WebSocket) {<br>    const { name: toolName, arguments: args } = msg.params ?? {};<br>    const tool = tools.get(toolName);<br><br>    if (!tool) {<br>        sendError(client, msg.id, -32601, `Tool &quot;${toolName}&quot; not found`);<br>        return;<br>    }<br><br>    try {<br>        const output = await tool.execute(args);<br>        sendResult(client, msg.id, { output });<br>    } catch (err: any) {<br>        sendError(client, msg.id, -32000, err?.message ?? &quot;Unknown tool error&quot;);<br>    }<br>}<br><br>export default async function controller(msg: any, client: WebSocket) {<br>    if (!msg || msg.jsonrpc !== &quot;2.0&quot;) {<br>        sendError(client, null, -32700, &quot;Invalid JSON-RPC format&quot;);<br>        return;<br>    }<br><br>    const { method } = msg;<br><br>    switch (method) {<br>        case &quot;initialize&quot;:<br>            handleInitialize(msg, client);<br>            break;<br><br>        case &quot;tools/list&quot;:<br>            handleToolList(msg, client);<br>            break;<br><br>        case &quot;tools/call&quot;:<br>            await handleCallTool(msg, client);<br>            break;<br><br>        default:<br>            sendError(client, msg.id, -32601, `Unknown method: ${method}`);<br>    }<br>}</pre><p>The list of tools is defined as a simple hash map. Each tool, according to the MCP protocol, must include:</p><ul><li><strong>name</strong> — so the client can reference it</li><li><strong>description</strong> — so the LLM model knows when to use it</li><li><strong>input schema</strong> — so the LLM model knows how to call it</li><li><strong>output schema</strong> — so the LLM model and the client know what to expect in the response</li></ul><p>In this example, the following tools are defined:</p><ul><li><strong>add</strong> — a basic math operation</li><li><strong>sql_query</strong> — runs SQL queries generated by the AI</li><li><strong>system_health_check</strong> — executes server-side scripts to check system status</li><li><strong>api_docs</strong> — returns API documentation</li><li><strong>send_email</strong> — sends an email using SMTP</li></ul><p>Here is the code that registers these tools:</p><pre>import nodemailer from &#39;nodemailer&#39;;<br>import {queryMySQL} from &#39;../utils/index.js&#39;;<br>import {runSystemHealthCheck} from &#39;../utils/healthCheck.js&#39;;<br>import {readLoginDocs, readGroupsDocs} from &#39;../utils/readFile.js&#39;;<br><br>export const tools = new Map&lt;string, ToolDefinition&gt;();<br><br>type ToolDefinition = {<br>    name: string;<br>    description?: string;<br>    inputSchema?: any;<br>    outputSchema?: any;<br>    execute: (input: any) =&gt; Promise&lt;any&gt;;<br>};<br><br>function registerTool(name: string, meta: any, handler: any) {<br>    tools.set(name, { name, ...meta, execute: handler });<br>}<br><br>registerTool(<br>    &#39;add&#39;,<br>    {<br>        title: &#39;Addition Tool&#39;,<br>        description: &#39;Add two numbers&#39;,<br>        inputSchema: {<br>            type: &#39;object&#39;,<br>            properties: {<br>                a: { type: &#39;number&#39; },<br>                b: { type: &#39;number&#39; }<br>            },<br>            required: [&#39;a&#39;, &#39;b&#39;]<br>        },<br>        outputSchema: {<br>            type: &#39;object&#39;,<br>            properties: { result: { type: &#39;number&#39; } },<br>            required: [&#39;result&#39;]<br>        }<br>    },<br>    async ({ a, b }: { a: number; b: number }) =&gt; {<br>        const result = { result: a + b };<br>        return {<br>            content: [{ type: &#39;text&#39;, text: JSON.stringify(result) }],<br>        };<br>    }<br>);<br><br>registerTool(<br>    &#39;sql_query&#39;,<br>    {<br>        title: &#39;SQL Query Tool&#39;,<br>        description: &#39;Execute SQL query&#39;,<br>        inputSchema: {<br>            type: &#39;object&#39;,<br>            properties: { query: { type: &#39;string&#39; } },<br>            required: [&#39;query&#39;]<br>        },<br>        outputSchema: { type: &#39;array&#39;, items: { type: &#39;object&#39; } }<br>    },<br>    async ({ query }: { query: string }) =&gt; {<br>        // put your database credentials here<br>        const result = await queryMySQL(<br>            { password: &#39;mypassword&#39;, user: &#39;myuser&#39;, host: &#39;localhost&#39;, database: &#39;mydatabase&#39; },<br>            query<br>        );<br>        return {<br>            content: [{ type: &#39;text&#39;, text: JSON.stringify(result) }],<br>        };<br>    }<br>);<br><br>registerTool(<br>    &#39;system_health_check&#39;,<br>    {<br>        title: &#39;System Health Check Tool&#39;,<br>        description: &#39;Return system health status&#39;,<br>        inputSchema: { type: &#39;object&#39;, properties: {} },<br>        outputSchema: { type: &#39;object&#39; }<br>    },<br>    async () =&gt; {<br>        const result = runSystemHealthCheck();<br>        return {<br>            content: [{ type: &#39;text&#39;, text: JSON.stringify(result, null, 2) }],<br>        };<br>    }<br>);<br><br>registerTool(<br>    &#39;api_docs&#39;,<br>    {<br>        title: &#39;API Documentation Tool&#39;,<br>        description: &#39;Return API documentation&#39;,<br>        inputSchema: { type: &#39;object&#39;, properties: {} },<br>        outputSchema: { type: &#39;object&#39; }<br>    },<br>    async () =&gt; {<br>        const result = await readLoginDocs();<br>        return {<br>            content: [{ type: &#39;text&#39;, text: JSON.stringify(result, null, 2) }],<br>        };<br>    }<br>);<br><br>registerTool(<br>    &#39;send_email&#39;,<br>    {<br>        title: &#39;Send Email Tool&#39;,<br>        description: &#39;Send email using SMTP via Nodemailer&#39;,<br>        inputSchema: {<br>            type: &#39;object&#39;,<br>            properties: {<br>                to: { type: &#39;string&#39;, format: &#39;email&#39; },<br>                subject: { type: &#39;string&#39; },<br>                text: { type: &#39;string&#39; }<br>            },<br>            required: [&#39;to&#39;, &#39;subject&#39;, &#39;text&#39;]<br>        },<br>        outputSchema: {<br>            type: &#39;object&#39;,<br>            properties: { result: { type: &#39;string&#39; } },<br>            required: [&#39;result&#39;]<br>        }<br>    },<br>    async ({ to, subject, text }: {to: string, subject: string, text: string}) =&gt; {<br>        try {<br>            // put here your email servce implementation<br>            const smtpUrl = &#39;smtp://localhost:25&#39;;<br><br>            const transporter = nodemailer.createTransport(smtpUrl);<br><br>            const mailOptions = {<br>                from: &#39;no-reply@gmail.com&#39;,<br>                to,<br>                subject,<br>                text<br>            };<br><br>            await transporter.sendMail(mailOptions);<br><br>            return { result: &#39;Email sent successfully&#39; };<br>        } catch (error: any) {<br>            return { result: `Error sending email: ${error.message}` };<br>        }<br>    }<br>);</pre><h3>MCP Client Implementation</h3><p>To interact with the MCP server, we use an MCP client. In this example, the client is a CLI application that reads user input from the console and orchestrates communication between the MCP server and the AI model.</p><p>The following code shows a simple console interface that reads user input, processes it, and prints the results:</p><pre>import {processQuery} from &#39;./lib/models/queryProcessor.js&#39;;<br><br>async function main() {<br>    console.log(&#39;🧩 MCP + OpenAI Client Ready! Type your query (or &quot;exit&quot;).&#39;);<br>    process.stdin.setEncoding(&#39;utf8&#39;);<br><br>    const ask = () =&gt; {<br>        process.stdout.write(&#39;\nYou: &#39;);<br>        process.stdin.once(&#39;data&#39;, async (input: string) =&gt; {<br>            const query = input.trim();<br>            if (query.toLowerCase() === &#39;exit&#39;) process.exit(0);<br>            try {<br>                const response = await processQuery(query);<br>                console.log(&#39;\n🤖 LLM:&#39;, response);<br>            } catch (err: any) {<br>                console.error(&#39;❌ Error:&#39;, err.message);<br>            }<br>            ask();<br>        });<br>    };<br>    ask();<br>}<br><br>main();</pre><p>The processQuery defines how the client sends messages between the MCP server and the LLM.<br> Each query begins with requesting the list of MCP tools. After receiving the tool list and the user query, the client builds a system prompt that defines the LLM’s behavior. The client then sends everything to the LLM.</p><p>The LLM responds with a list of tool calls that must be executed. The client sends these tool requests to the MCP server. This loop continues for several iterations until the LLM stops requesting tool calls. After that, the query processor returns the final answer.</p><pre>import {callMcp} from &#39;./mcpClient.js&#39;;<br>import {callAi, Message, Tool} from &#39;./aiClient.js&#39;;<br><br>type ToolSchema = {<br>    name: string,<br>    description: string,<br>    inputSchema: Record&lt;string, any&gt;<br>};<br><br>async function getMcpTools(): Promise&lt;Tool[]&gt; {<br>    const toolsData = await callMcp(&quot;tools/list&quot;);<br><br>    return toolsData.tools.map((t: ToolSchema) =&gt; ({<br>        type: &quot;function&quot;,<br>        function: {<br>            name: t.name,<br>            description: t.description,<br>            parameters: t.inputSchema || {}<br>        }<br>    }));<br>}<br><br>export async function processQuery(userQuery: string) {<br>    const tools: Tool[] = await getMcpTools();<br><br>    const systemPrompt = `<br>You are an intelligent assistant connected to a tool system.<br>When appropriate, call a tool using a JSON function call.<br>Available tools:<br>${tools.map((t: Tool) =&gt; `- ${t.function.name}: ${t.function.description}`).join(&quot;\n&quot;)}<br>    `;<br><br>    let messages: Message[] = [<br>        { role: &quot;assistant&quot;, content: systemPrompt, name: &quot;system&quot; },<br>        { role: &quot;user&quot;, content: userQuery, name: &#39;user&#39; }<br>    ];<br>    let response = await callAi(messages, tools);<br><br>    while (response?.tool_calls?.length) {<br>        for (const call of response.tool_calls) {<br>            const { name, arguments: argStr } = call.function;<br>            const args = argStr ? JSON.parse(argStr) : {};<br><br>            const toolResult = await callMcp(&quot;tools/call&quot;, { name, arguments: args });<br><br>            messages.push({<br>                role: &quot;function&quot;,<br>                name,<br>                content: JSON.stringify(toolResult)<br>            });<br>        }<br><br>        // Ask the AI again with updated messages<br>        response = await callAi(messages, tools);<br>    }<br><br>    return response.content ?? &quot;No response.&quot;;<br>}</pre><p>The <strong>MCP caller</strong> is a simple WebSocket client that sends JSON-RPC requests. On first MCP call it establish WebSocket connection with the server and use it for communication further.</p><pre>import WebSocket from &#39;ws&#39;;<br>import {v4 as uuidv4} from &#39;uuid&#39;;<br>import {config} from &#39;../config.js&#39;;<br><br>let ws: WebSocket | null = null;<br>let connected = false;<br><br>const pending = new Map&lt;string, (msg: any) =&gt; void&gt;();<br><br>async function ensureConnected(): Promise&lt;void&gt; {<br>    if (connected &amp;&amp; ws) return;<br><br>    ws = new WebSocket(config.serverUrl);<br><br>    await new Promise&lt;void&gt;((resolve, reject) =&gt; {<br>        ws!.once(&quot;open&quot;, resolve);<br>        ws!.once(&quot;error&quot;, reject);<br>    });<br><br>    ws.on(&quot;message&quot;, (raw) =&gt; {<br>        let msg;<br>        try {<br>            msg = JSON.parse(raw.toString());<br>        } catch {<br>            console.error(&quot;❌ Invalid JSON from MCP:&quot;, raw.toString());<br>            return;<br>        }<br><br>        if (msg.id &amp;&amp; pending.has(msg.id)) {<br>            pending.get(msg.id)!(msg);<br>            pending.delete(msg.id);<br>            return;<br>        }<br>    });<br><br>    const initMsg = {<br>        jsonrpc: &quot;2.0&quot;,<br>        id: uuidv4(),<br>        method: &quot;initialize&quot;,<br>        params: {<br>            clientInfo: { name: &quot;openai-client&quot;, version: &quot;1.0.0&quot; },<br>            protocolVersion: &quot;2024-10-14&quot;,<br>            capabilities: {}<br>        }<br>    };<br><br>    ws.send(JSON.stringify(initMsg));<br><br>    await new Promise&lt;void&gt;((resolve) =&gt; {<br>        const handler = (raw: any) =&gt; {<br>            const msg = JSON.parse(raw.toString());<br><br>            if (msg.method === &quot;notifications/initialized&quot;) {<br>                ws!.off(&quot;message&quot;, handler);<br>                connected = true;<br>                resolve();<br>            }<br>        };<br><br>        ws!.on(&quot;message&quot;, handler);<br>    });<br><br>    console.log(&quot;🔌 Connected to MCP server&quot;);<br>}<br><br>function send(msg: any): Promise&lt;any&gt; {<br>    const id = msg.id;<br><br>    return new Promise((resolve) =&gt; {<br>        pending.set(id, resolve);<br>        ws!.send(JSON.stringify(msg));<br>    });<br>}<br><br>export async function callMcp(action: &quot;tools/list&quot; | &quot;tools/call&quot;, params: any = {}) {<br>    await ensureConnected();<br>    const id = uuidv4();<br><br>    if (action === &quot;tools/list&quot;) {<br>        const res = await send({<br>            jsonrpc: &quot;2.0&quot;,<br>            id,<br>            method: &quot;tools/list&quot;,<br>            params: {}<br>        });<br><br>        if (res.error) {<br>            throw new Error(res.error.message);<br>        }<br><br>        return res.result;<br>    }<br><br>    if (action === &quot;tools/call&quot;) {<br>        const res = await send({<br>            jsonrpc: &quot;2.0&quot;,<br>            id,<br>            method: &quot;tools/call&quot;,<br>            params: {<br>                name: params.name,<br>                arguments: params.arguments<br>            }<br>        });<br><br>        if (res.error) {<br>            throw new Error(res.error.message);<br>        }<br><br>        return res.result;<br>    }<br><br>    throw new Error(&quot;Unknown MCP action: &quot; + action);<br>}</pre><p>The callAi uses the OpenAI library. In this example, the model gpt-4.1 is used. The next section of the article will explain how to generate an API key.</p><pre>import {OpenAI} from &#39;openai&#39;;<br>import {ChatCompletionMessage} from &#39;openai/resources/chat/completions/completions&#39;;<br>import {config} from &#39;../config.js&#39;;<br><br>export type Message = {<br>    role: &#39;system&#39; | &#39;user&#39; | &#39;assistant&#39; | &#39;function&#39;;<br>    content: string;<br>    name: string;<br>};<br>export type Tool = {<br>    type: &quot;function&quot;,<br>    function: {<br>        name: string,<br>        description: string,<br>        parameters: any<br>    }<br>};<br><br>const openai = new OpenAI({ apiKey: config.openAIApiKey });<br><br>export async function callAi(messages: Message[], tools: Tool[]): Promise&lt;ChatCompletionMessage&gt; {<br>    const response = await openai.chat.completions.create({<br>        model: &quot;gpt-4.1&quot;,<br>        messages,<br>        tools,<br>        tool_choice: &quot;auto&quot;<br>    });<br><br>    return response.choices[0].message;<br>}</pre><p>With this simple MCP client and server setup, we get a powerful integration with OpenAI, enabling automatic tool selection, execution, and response handling.</p><h3>OpentAI Configuration</h3><p>To start using the solution described above, you need to create an OpenAI API key openAIApiKey mentioned in secion above.<br> To generate the key, log in to the <a href="https://platform.openai.com/docs/overview"><strong>OpenAI Platform</strong></a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*J0oMohfElM7o9-dNITgqEA.png" /></figure><p>In the left sidebar, click <strong>API Keys</strong>, then in the top-right corner click <strong>Create new secret key</strong>.<br> Copy this key — it will be used by your client.</p><p>Using the API is not free, so you need to add some funds to your account. The minimum amount for the moment is <strong>$5</strong>, and this is more than enough to experiment with this setup. For example, 30,000 tokens cost me around <strong>$0.10</strong>.<br> To add credit, navigate to the <strong>Billing</strong> tab and follow the simple instructions on the page to fill your credit balance.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2fNFQZyjHMzDkiKSun4z_Q.png" /></figure><p>This is a relatively cheap way to start working with the solution.<br> A free alternative is <strong>Ollama</strong>, which runs locally on your own hardware. I tested this option, but because I don’t have powerful hardware, the models in Ollama performed poorly for me. After switching to OpenAI, the quality of responses improved significantly and really surprised me.</p><p>Of course, if you use Ollama instead of OpenAI, the callAi function and the previous section would need to be updated accordingly.</p><h3>Demo</h3><p>This section shows screenshots of user prompts and the results returned by OpenAI based on the data provided by the MCP server.</p><p>In the first example, the user requests the list of available tools, and the system returns all tools that can be used.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cjTEssM_ae1M3vhGgaKuSQ.png" /></figure><p>In the next example, the user asks the system to perform a health check and apply some custom criteria for sending an email notification. This demonstrates how the client performs multiple iterations with OpenAI to call the required tools.</p><p>Initially, OpenAI calls the system_health_check tool. Once the client sends the results back, along with the updated context, the model requests the send_email tool. The MCP server executes the email action, and the user receives the message.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wV2ZnoLeQ9PC9QqgXZfYAg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*k_1OcURxCbMqfwgoGdTJEQ.png" /></figure><p>Another example shows that OpenAI reads the API documentation and provides guidance on how to use it, including generating a sample curl command.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_Ma6W70fwr6fNyECCNn9gg.png" /></figure><h3>Conclusion</h3><p>In this article, you built an <strong>MCP Server</strong> in <strong>TypeScript</strong>, connected it to <strong>ChatGPT</strong>, and exposed tools for real-world actions like database queries and system checks. MCP is changing how we interact with LLMs, turning them from conversational agents into operational assistants. Its growing popularity is reflected in integrations with Docker Desktop, Cursor IDE, and VS Code extensions.</p><p>As next steps for enhancing the described solution, consider implementing a layered architecture, adding authentication, exposing more complex APIs, or deploying the MCP server with Docker — opening the door to broader AI-driven workflows.</p><p>An example of the implemented solution can be <a href="https://github.com/yzhbankov/mcp-server">found in [2]</a>.</p><h3>Referense</h3><ol><li><a href="https://modelcontextprotocol.io/docs/getting-started/intro">https://modelcontextprotocol.io/docs/getting-started/intro</a></li><li><a href="https://github.com/yzhbankov/mcp-server">https://github.com/yzhbankov/mcp-server</a></li><li><a href="https://cloud.google.com/discover/what-is-model-context-protocol?utm_source=chatgpt.com">https://cloud.google.com/discover/what-is-model-context-protocol?utm_source=chatgpt.com</a></li><li><a href="https://www.ibm.com/think/topics/model-context-protocol?utm_source=chatgpt.com">https://www.ibm.com/think/topics/model-context-protocol?utm_source=chatgpt.com</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=06047bfc41f8" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Node.js at Scale: Handling 100,000 Msg/sec from 50,000 Emulated IoT Devices with Redis and ZMQ…]]></title>
            <link>https://medium.com/@yaroslavzhbankov/high-throughput-node-js-8bc87782d67c?source=rss-88b741f7b069------2</link>
            <guid isPermaLink="false">https://medium.com/p/8bc87782d67c</guid>
            <category><![CDATA[nodejs]]></category>
            <category><![CDATA[optimization]]></category>
            <category><![CDATA[scalability]]></category>
            <category><![CDATA[redis]]></category>
            <category><![CDATA[iot]]></category>
            <dc:creator><![CDATA[Dr. Yaroslav Zhbankov]]></dc:creator>
            <pubDate>Mon, 29 Sep 2025 01:42:51 GMT</pubDate>
            <atom:updated>2025-09-29T02:05:57.591Z</atom:updated>
            <content:encoded><![CDATA[<h3>Node.js at Scale: Handling 100,000 Msg/sec from 50,000 Emulated IoT Devices with Redis and ZMQ Without Horizontal Scaling</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kpMr4JHdpW530bRODbT8Pw.png" /></figure><h3>Introduction</h3><p>Service-oriented architecture (SOA) often relies on lightweight message buses like <strong>ZeroMQ</strong> (ZMQ) to enable communication between services. Node.js is a popular choice for implementing services due to its simplicity and asynchronous capabilities. However, <strong>Node.js</strong> comes with inherent limitations for CPU-bound or high-throughput tasks.</p><p>In theory, a simple <strong>Node.js</strong> service using in-memory <strong>ZMQ</strong> messages can handle up to ~1 million messages per second in an ideal push-pull setup, assuming very small messages (&lt;200 bytes) and minimal processing. In practice, throughput is constrained by factors such as message size, CPU-bound operations, <strong>Redis</strong> interactions, network latency, and message serialization/deserialization.</p><p>This article demonstrates how to scale message throughput for a <strong>Node.js</strong> service that consumes messages, interacts asynchronously with <strong>Redis</strong>, and forwards messages to the next consumer. Using worker threads and batching, it will be shown how to reach <strong>~100,000</strong> messages per second for <strong>50,000</strong> unique keys stored in Redis <strong>— each key representing an emulated IoT device identifier sending messages at a given rate — </strong>without relying on horizontal scaling. While even higher throughput could be achieved using Kafka, C++, or clustered Redis, this article focuses on optimizing Node.js, Redis, and ZMQ for simplicity and practical relevance.</p><h3>System Limitations</h3><ul><li><strong>Node.js</strong> is single-threaded and asynchronous, meaning I/O operations (like network or Redis calls) do not block the event loop. CPU-bound tasks, however, block processing and limit throughput. Worker threads, child processes, and clustering can parallelize CPU-bound work and increase performance.</li><li><strong>ZMQ</strong> is a lightweight messaging protocol over TCP, capable of millions of messages per second under <a href="http://wiki.zeromq.org/results:100gbe-tests-v432">ideal conditions [1]</a>. Actual throughput depends on message size, socket type, network latency, and system resources.</li><li><strong>Redis</strong> is a <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/benchmarks/">performance in-memory store [2]</a>. Single-core operations typically achieve 100k–1M ops/sec; multi-core usage, pipelining, and batching can raise throughput to several million ops/sec. Operations like HGETALL on large hashes are particularly expensive.</li><li><strong>Practical throughput estimates (for ~100-byte messages): <br></strong>Node.js: 300k–600k msg/sec<br>Redis: 300k–700k ops/sec (without pipelining)<br>ZMQ: 800k–1.5M msg/sec (based on <strong>native benchmarks</strong>)</li></ul><p><strong>Note:</strong> Achieving these maxima simultaneously is difficult, as the slowest component dictates overall throughput. Message handling, waiting for responses, ordering, and blocking operations further reduce performance.</p><p>Next, these limitations will be examined in a practical example. You can find the Node.js setup in the <a href="https://github.com/yzhbankov/arch">GitHub repository [3]</a>.</p><h3>Practical Implementation Overview</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PzORVmH1MLanaCDheg5V_g.png" /></figure><p>The aim is to implement a <strong>message processor</strong> service that:</p><ol><li>Consumes messages using a <strong>ZMQ PULL socket</strong>.</li><li>Parses and extends messages with a timestamp.</li><li>Interacts with Redis: reads existing records, deletes old ones, and stores new messages.</li><li>Forwards processed messages via a <strong>ZMQ PUSH socket</strong> to the next consumer.</li></ol><p>This example demonstrates throughput optimization in Node.js using worker threads and batch processing. It is <strong>important to note</strong> that records are stored in Redis by unique key, and the number of unique keys will be equal to half the number of messages sent per second (e.g., 10,000 messages per second will generate 5,000 unique keys, resulting in 5,000 records in Redis), <strong>emulating IoT devices sending messages at a 2 Hz frequency</strong>.</p><h4>Methodology</h4><ol><li><strong>Message Producer:</strong> Generates and sends messages via a ZMQ PUSH socket.</li><li><strong>Message Processor:</strong> Receives messages, performs Redis operations, and forwards messages.</li><li><strong>Final Consumer:</strong> Reads messages using a ZMQ PULL socket and tracks throughput.</li></ol><h3>Message Producer</h3><p>The Node.js producer sends messages with configured frequency. It generates messages with a simple structure — objects containing a key, timestamp, and some random data — stringifies them, and sends them using the ZMQ <strong>PUSH</strong> protocol.</p><pre>import { Push } from &quot;zeromq&quot;;<br><br>const ADDRESS = &quot;tcp://127.0.0.1:7001&quot;;<br>const BATCH_SIZE = 1;<br>const MSGS_PER_SECOND = 6500;<br>const KEY_RANGE = 3250;<br>const DELAY_NANOSECONDS = BigInt(Math.floor(1e9 / MSGS_PER_SECOND));<br><br>const keys = Array.from({ length: KEY_RANGE }, (_, i) =&gt;<br>    String(i + 1).padStart(8, &quot;0&quot;) + &quot;A&quot;<br>);<br><br>function logStats(counter, totalMessages) {<br>    console.log(`Messages sent in last 1s: ${counter.value}`);<br>    console.log(`Total messages sent: ${totalMessages.value}`);<br>    counter.value = 0;<br>}<br><br>function createMessage() {<br>    const key = keys[Math.floor(Math.random() * KEY_RANGE)];<br>    return JSON.stringify({<br>        key,<br>        ts: new Date().toISOString(),<br>        data: {<br>            randomInt: Math.floor(Math.random() * 100),<br>        },<br>    });<br>}<br><br>async function handleExit(push, totalMessages) {<br>    console.log(`\nProcess exiting. Total messages sent: ${totalMessages.value}`);<br>    try {<br>        await push.close();<br>    } catch (err) {<br>        console.error(&quot;Error closing Push socket:&quot;, err);<br>    }<br>    process.exit(0);<br>}<br><br>async function runPublisher() {<br>    const push = new Push();<br>    await push.bind(ADDRESS);<br>    console.log(`Push publisher bound to ${ADDRESS}`);<br><br>    const counter = { value: 0 };<br>    const totalMessages = { value: 0 };<br><br>    setInterval(() =&gt; logStats(counter, totalMessages), 1000);<br><br>    const exitHandler = () =&gt; handleExit(push, totalMessages);<br>    process.on(&quot;SIGINT&quot;, exitHandler);<br>    process.on(&quot;SIGTERM&quot;, exitHandler);<br><br>    let next = process.hrtime.bigint();<br>    while (true) {<br>        const now = process.hrtime.bigint();<br>        if (now &gt;= next) {<br>            const batch = BATCH_SIZE &gt; 1 ? Array.from({ length: BATCH_SIZE }, createMessage) : createMessage();<br>            await push.send(batch);<br>            counter.value += BATCH_SIZE;<br>            totalMessages.value += BATCH_SIZE;<br>            next += DELAY_NANOSECONDS;<br>        }<br>    }<br>}<br><br>runPublisher().catch(console.error);</pre><p><strong>Important Notes:</strong></p><ul><li>The producer binds the port and sends messages whenever a consumer is ready to pull them.</li><li>This producer will be used for multiple scenarios in this article to test throughput.</li></ul><h3>Final Consumer</h3><p>The consumer reads messages after they have been processed by the message processor. It uses a ZMQ <strong>PULL</strong> socket to receive messages, parses them, and keeps track of throughput statistics.</p><pre>import { Pull } from &quot;zeromq&quot;;<br><br>const ADDRESS = &quot;tcp://127.0.0.1:7000&quot;;<br><br>function logStats(counter, totalMessages) {<br>    console.log(`Messages received in last 1s: ${counter.value}`);<br>    console.log(`Total messages received: ${totalMessages.value}`);<br>    counter.value = 0;<br>}<br><br>async function handleExit(pull, totalMessages) {<br>    console.log(`\nProcess exiting. Total messages received: ${totalMessages.value}`);<br>    try {<br>        await pull.close();<br>    } catch (err) {<br>        console.error(&quot;Error closing Pull socket:&quot;, err);<br>    }<br>    process.exit(0);<br>}<br><br>async function runConsumer() {<br>    const pull = new Pull();<br>    await pull.bind(ADDRESS);<br>    console.log(`Pull consumer bound to ${ADDRESS}`);<br><br>    const counter = { value: 0 };<br>    const totalMessages = { value: 0 };<br><br>    setInterval(() =&gt; logStats(counter, totalMessages), 1000);<br><br>    const exitHandler = () =&gt; handleExit(pull, totalMessages);<br>    process.on(&quot;SIGINT&quot;, exitHandler);<br>    process.on(&quot;SIGTERM&quot;, exitHandler);<br><br>    for await (const msg of pull) {<br>        try {<br>            JSON.parse(msg.toString());<br>            counter.value += 1;<br>            totalMessages.value += 1;<br>        } catch (err) {<br>            console.error(&quot;Failed to parse batch:&quot;, err);<br>        }<br>    }<br>}<br><br>runConsumer().catch(console.error);</pre><p>Important Notes:</p><ul><li>The consumer binds a port and reads messages sent by the message processor.</li><li>It will be used in multiple scenarios in this article to measure throughput and message handling performance.</li></ul><h3>Solution 1 — Single-Threaded Node.js Message Processor</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mvYHqLwHIHbkc41fhw2o_A.png" /></figure><p>The producer and consumer can generate and handle a massive number of messages. Therefore, the main focus should be on the Message Processor, which performs parsing, Redis operations, and message forwarding via ZMQ. Redis itself can become a bottleneck, and as mentioned above, the number of records in Redis will equal half of the message frequency, emulating a 2 Hz message rate per single object. This is similar to some device sending 2 messages per second to a server — so 10,000 messages per second would correspond to 5,000 devices with unique keys.</p><p>The single-threaded message processor operates as follows:</p><pre>for await (const [msg] of zmqPullClient.socket) {<br>  try {<br>    counter.value += 1;<br>    totalMessages.value += 1;<br>    await handleMessage(msg, dbConnection, zmqPushClient.socket);<br>  } catch (err) {<br>    console.error(&#39;Handler failed:&#39;, err);<br>  }<br>}<br><br>async function handleMessage(messageRaw, client, socket) {<br>  let parsedMsg;<br>  try {<br>    parsedMsg = typeof messageRaw === &#39;string&#39; ? JSON.parse(messageRaw) : JSON.parse(messageRaw.toString());<br>  } catch (err) {<br>    console.error(&#39;Failed to parse message:&#39;, err);<br>    return;<br>  }<br><br>  const redisKey = `test:${parsedMsg.key}`;<br>  try {<br>    await client.hGetAll(redisKey);<br>    await client.del(redisKey);<br>    await client.hSet(redisKey, [&#39;field1&#39;, JSON.stringify(parsedMsg)]);<br>  } catch (err) {<br>    console.error(&#39;Redis operation failed:&#39;, err);<br>    return;<br>  }<br><br>  try {<br>    const response = { ...parsedMsg, ts: new Date().toISOString() };<br>    await socket.send(JSON.stringify(response));<br>  } catch (err) {<br>    console.error(&#39;Failed to send message via ZMQ socket:&#39;, err);<br>  }<br>}</pre><p>This single-threaded solution handles <strong>~7,000 msg/sec</strong> and <strong>3,500 unique keys</strong> in Redis with no delay that is not very impressive and definitely requires improvement.</p><h3>Solution 2 — Main Thread for Receiving and Worker for Processing</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AOFhcOc1C4D6axvL0PuauQ.png" /></figure><p>The idea here is to decouple <strong>receiving messages</strong> from <strong>processing them</strong>, which reduces idle time caused by Redis operations and ZMQ forwarding.</p><p>The main thread pulls messages and sends them to a worker via IPC:</p><pre>export async function main() {<br>    const __filename = fileURLToPath(import.meta.url);<br>    const __dirname = path.dirname(__filename);<br>    let worker;<br><br>    const zmqPullClient = new ZmqPullClient({ address: &#39;tcp://127.0.0.1:7001&#39; });<br>    await zmqPullClient.start();<br><br>    // Track worker readiness<br>    await new Promise((resolve) =&gt; {<br>        worker = new Worker(path.resolve(__dirname, &#39;worker.mjs&#39;));<br>        worker.on(&#39;message&#39;, (msg) =&gt; {<br>            if (msg &amp;&amp; msg.ready) {<br>                resolve();<br>            }<br>        });<br>    });<br><br>    for await (const msg of zmqPullClient.socket) {<br>        worker.postMessage(msg.toString());<br>    }<br>}</pre><p>The worker implements the same Redis read/delete/set logic as in first solution:</p><pre>parentPort?.on(&#39;message&#39;, async (msg) =&gt; {<br>    try {<br>        await handleMessage(msg, dbConnection, { send: safeSend });<br>        parentPort?.postMessage({ ok: true });<br>    } catch (err) {<br>        console.error(&#39;Worker error:&#39;, err);<br>        parentPort?.postMessage({ ok: false, error: err?.message || err });<br>    }<br>});<br><br>async function handleMessage(messageRaw, client, socket) {<br>    let parsedMsg;<br>    try {<br>        parsedMsg = typeof messageRaw === &#39;string&#39; ? JSON.parse(messageRaw) : JSON.parse(messageRaw.toString());<br>    } catch (err) {<br>        console.error(&#39;Failed to parse message:&#39;, err);<br>        return;<br>    }<br><br>    const redisKey = `test:${parsedMsg.key}`;<br>    try {<br>        await client.hGetAll(redisKey);<br>        await client.del(redisKey);<br>        await client.hSet(redisKey, [&#39;field1&#39;, JSON.stringify(parsedMsg)]);<br>    } catch (err) {<br>        console.error(&#39;Redis operation failed:&#39;, err);<br>        return;<br>    }<br><br>    try {<br>        const response = { ...parsedMsg, ts: new Date().toISOString() };<br>        await socket.send(JSON.stringify(response));<br>    } catch (err) {<br>        console.error(&#39;Failed to send message via ZMQ socket:&#39;, err);<br>    }<br>}</pre><p>This approach achieves <strong>~20,000 msg/sec</strong> with <strong>10,000 unique keys</strong> in Redis without queuing or delays, which is already three times better simply by using a separate process to handle messages.</p><h3>Solution 3 — Main Thread with Multiple Workers</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FgXQcB9dDdVRXWJeaiXLWA.png" /></figure><p>To further increase throughput, multiple workers can process messages in parallel. Messages are distributed to workers in a round-robin manner:</p><pre>import path from &#39;path&#39;;<br>import { fileURLToPath } from &#39;url&#39;;<br>import { Worker } from &#39;worker_threads&#39;;<br>import { ZmqPullClient } from &#39;./ZmqPullClient.mjs&#39;;<br><br><br>const WORKER_COUNT = 2;<br>const workers = [];<br>let nextWorker = 0;<br>let readyWorkers = 0;<br><br>function getWorker() {<br>    const w = workers[nextWorker];<br>    nextWorker = (nextWorker + 1) % workers.length;<br>    return w;<br>}<br><br>export async function main() {<br>    const __filename = fileURLToPath(import.meta.url);<br>    const __dirname = path.dirname(__filename);<br><br>    const zmqPullClient = new ZmqPullClient({ address: &#39;tcp://127.0.0.1:7001&#39; });<br>    await zmqPullClient.start();<br><br>    // Track worker readiness<br>    await new Promise((resolve) =&gt; {<br>        for (let i = 0; i &lt; WORKER_COUNT; i++) {<br>            const worker = new Worker(path.resolve(__dirname, &#39;worker.mjs&#39;));<br>            worker.on(&#39;message&#39;, (msg) =&gt; {<br>                if (msg &amp;&amp; msg.ready) {<br>                    readyWorkers++;<br>                    if (readyWorkers === WORKER_COUNT) {<br>                        resolve();<br>                    }<br>                }<br>            });<br>            workers.push(worker);<br>        }<br>    });<br><br>    for await (const msg of zmqPullClient.socket) {<br>        getWorker().postMessage(msg.toString());<br>    }<br>}<br><br>main();</pre><ul><li>Max throughput increases to <strong>~35,000 msg/sec</strong> and <strong>17,500 unique keys</strong> in Redis with just two workers.</li><li>Adding more workers does not improve throughput, indicating the main thread’s message dispatching is the limiting factor. In Node.js worker_threads, posting messages (worker.postMessage) itself adds overhead.</li><li>Compared to Solution 1, this represents a 7X improvement.</li></ul><h3>Solution 4 — Multiple Workers with Batch Messages</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AXm9guRmclpFAM1u3dmnSA.png" /></figure><p>Throughput can be further improved by processing messages in batches. The producer sends multiple messages in a single package, the message processor distributes the batch to workers, and the batch is sent to the next consumer.</p><pre>for await (const msgList of zmqPullClient.socket) {<br>  const messages = msgList.map((msg) =&gt; msg.toString());<br>  getWorker().postMessage(messages);<br>}</pre><pre>parentPort?.on(&#39;message&#39;, async (msgList) =&gt; {<br>    try {<br>        const responses = [];<br>        for (const singleMsg of msgList) {<br>            const result = await handleMessage(JSON.parse(singleMsg), dbConnection);<br>            responses.push(result);<br>        }<br>        counter += msgList.length;<br>        await safeSend(JSON.stringify(responses));<br>        parentPort?.postMessage({ ok: true });<br>    } catch (err) {<br>        console.error(&#39;Worker error:&#39;, err);<br>        parentPort?.postMessage({ ok: false, error: err?.message || err });<br>    }<br>});</pre><ul><li>Using 4 workers and a batch size of <strong>10 messages</strong> increases throughput to ~<strong>100,000 msg/sec </strong>with <strong>50,000 unique keys</strong> in Redis.</li><li>Reducing number of unique Redis keys improving throughput.</li><li>Further increases are limited by Redis throughput and main thread dispatching.</li></ul><h3>Conclusion</h3><p>The strategies described demonstrate how to dramatically increase the throughput of a <strong>Node.js</strong> service using <strong>Redis</strong> and <strong>ZMQ</strong>, achieving up to <strong>100k messages/sec for 50k unique keys (emulating 50k IoT devices sending messages)</strong>. Major improvements come from:</p><ul><li>Parallelizing message handling across multiple worker threads.</li><li>Sending messages in batches to reduce inter-thread and network overhead.</li></ul><p>Further optimization opportunities include:</p><ul><li>Minimizing inter-thread communication overhead with <strong>transferable objects or shared memory</strong>.</li><li>Leveraging <strong>Node.js clustering</strong> to utilize all CPU cores for receiving and dispatching messages.</li><li>Exploring <strong>Redis sharding or cluster mode</strong> for even larger datasets.</li></ul><p>With these techniques, Node.js can handle high-throughput messaging workloads efficiently without requiring complex horizontal scaling.</p><h3>References</h3><ol><li><a href="http://wiki.zeromq.org/results:100gbe-tests-v432">http://wiki.zeromq.org/results:100gbe-tests-v432</a></li><li><a href="https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/benchmarks/">https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/benchmarks/</a></li><li><a href="https://github.com/yzhbankov/arch">https://github.com/yzhbankov/arch</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8bc87782d67c" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[SAML SSO with Microsoft Entra ID: A Practical Guide for Node.js and React Developers]]></title>
            <link>https://medium.com/@yaroslavzhbankov/saml-sso-with-microsoft-entra-id-a-practical-guide-for-node-js-and-react-developers-8300df860d5f?source=rss-88b741f7b069------2</link>
            <guid isPermaLink="false">https://medium.com/p/8300df860d5f</guid>
            <category><![CDATA[react]]></category>
            <category><![CDATA[sso]]></category>
            <category><![CDATA[aml]]></category>
            <category><![CDATA[azure-active-directory]]></category>
            <category><![CDATA[nodejs]]></category>
            <dc:creator><![CDATA[Dr. Yaroslav Zhbankov]]></dc:creator>
            <pubDate>Fri, 01 Aug 2025 02:35:43 GMT</pubDate>
            <atom:updated>2025-08-01T02:35:43.009Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FeMqqAIX6CqpBzbrLy9Azg.png" /></figure><h3>Introduction</h3><p>Single Sign-On (SSO) is commonly implemented in enterprise solutions using two widely adopted protocols: SAML and OAuth 2.0. The importance of SSO for enterprise systems — along with scenarios where it becomes critical — is discussed<a href="https://medium.com/@yaroslavzhbankov/implementing-oauth-2-0-authorization-code-flow-with-aws-cognito-for-enterprise-sso-e09d8f370981"> in [1]</a>, which also provides a complete example of an OAuth 2.0-based solution.</p><p>This article demonstrates how to configure SSO using Microsoft Entra ID (formerly Azure AD) in a server–client architecture, where multiple client applications — such as desktop and web apps — authenticate through a single backend using SAML. A SAML-based solution will be implemented using a simple Node.js server and a React-based frontend as an example.</p><h3>Solution overview</h3><p>As described above, the solution consists of a web application built with <strong>Next.js</strong> that provides the user interface, a <strong>Node.js (Express)</strong> web server that exposes a REST API for accessing resources, and <strong>Microsoft Entra ID</strong> (formerly Azure AD) as the centralized identity provider. The solution implements <strong>SAML authentication</strong> to enable Single Sign-On (SSO).</p><p>For this example, a few core requirements define how the solution is implemented:</p><ul><li>The system must support <strong>multiple client applications</strong>, such as different web and desktop apps, which allow users to access shared server resources.</li><li>It must also support <strong>multiple web servers</strong>, each serving a different customer (tenant).</li><li>Client applications must be able to interact with <strong>multiple servers (customers)</strong> simultaneously, without relying on hardcoded or customer-specific configuration.</li></ul><p>Given these constraints, client applications <strong>should not be aware</strong> of Microsoft Entra ID directly, as each customer may use a different identity provider. Therefore, the <strong>authentication logic and IdP configuration reside on the server</strong>, while the client follows a consistent authentication flow.</p><p>In this model, client applications authenticate <strong>through the server</strong>, which acts as a proxy to Microsoft Entra ID.</p><p>The server implements the following endpoints:</p><ul><li>GET /api/auth/login – Redirects the user to the Entra ID login UI.</li><li>GET /api/auth/logout – Clears the server session and redirects to the login page.</li><li>GET /api/protected – An example of a protected resource endpoint.</li><li>POST /api/auth/callback – Handles the SAML response and issues a JWT, setting it as a cookie.</li></ul><p>Below is a diagram illustrating the described solution (Fig. 1):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*l_-TfbFWM96x_TCxvOh46Q.png" /><figcaption>Fig. 1. Diagram of the SSO-enabled client–server solution</figcaption></figure><p>The steps shown in the diagram outline the authentication process:</p><ol><li>The user visits the web application and clicks the <strong>“Login with SAML”</strong> button.</li><li>The web client redirects the browser to the server’s /api/auth/login endpoint.</li><li>The server initiates a SAML authentication request (via passport-saml) and redirects the user to <strong>Microsoft Entra ID</strong> (the Identity Provider).</li><li>The user authenticates with Entra ID using their credentials.</li><li>Upon successful authentication, Entra ID returns a <strong>SAML Response</strong> (containing a signed assertion with user identity and attributes) to the server’s /api/auth/callback endpoint.</li><li>The server validates the SAML Response using passport-saml, extracts user attributes (e.g., name, email), and issues a <strong>JWT</strong> signed with a server secret.</li><li>The JWT is set as an <strong>HTTP-only cookie</strong>, and the user is redirected back to the client application.</li><li>The client is now authenticated and can access protected endpoints.</li><li>For each request to a protected resource (e.g., /api/protected), the server validates the JWT from the cookie.</li><li>If the token is valid, the server grants access and returns user-specific content.</li></ol><h3>Authentication Sequence: Step-by-Step Flow</h3><p>Below is a sequence diagram provides more detailed description of the authentication flow (Fig. 2).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/822/1*STVcDrj8T9BtylUoYEC2YA.png" /><figcaption>Fig. 2 The sequence diagram describing the authentication flow</figcaption></figure><p><strong>1. User Initiates Login</strong><br>- The user opens the application in their browser.<br>- The WebClient (frontend) displays a login interface with a “Login with SAML” button.</p><p><strong>2. Login Request to Server</strong><br>- When the user clicks the login button, the browser sends a request to the REST server: GET /api/auth/login</p><p><strong>3. Redirect to Microsoft Entra ID</strong><br>- The REST server generates a SAML authentication request.<br>- It sends back a 302 redirect to the browser pointing to Microsoft Entra ID’s login endpoint.<br>- The browser follows the redirect and sends the SAML request to Microsoft Entra ID.</p><p><strong>4. User Authenticates with Microsoft Entra ID</strong><br>- Microsoft Entra ID prompts the user to authenticate, e.g., by entering credentials or using SSO.<br>- Upon successful login, Entra ID responds with a SAML assertion containing user identity information.</p><p><strong>5. SAML Response Returned to Backend</strong><br>- The SAML response is sent to the browser, which then posts it to the backend: POST /api/auth/callback<br>- This request contains the SAML assertion.</p><p><strong>6. SAML Assertion Validation</strong><br>- The REST server uses Passport-SAML to validate the SAML response and extract user identity claims (like nameID, email, etc.).<br>- If validation succeeds, it creates a signed JWT token containing the user information.</p><p><strong>7. Session Established<br>- </strong>The backend sets a <strong>secure, HTTP-only cookie</strong> named auth_token with the JWT.<br>- It then redirects the browser back to the WebClient (frontend).</p><p><strong>8. Accessing Protected Resources<br>- </strong>The browser <strong>navigates to the home page</strong> or another protected route.<br>- The <strong>WebClient sends a request to a protected API endpoint</strong>: GET /api/protected including the auth_token cookie.<br>- The backend verifies the token and <strong>returns the authorized user info</strong>.<br>- The <strong>WebClient renders the page</strong> based on the authenticated user’s profile.</p><p><strong>9. Logout</strong><br>- When the user clicks logout, the <strong>WebClient sends a request</strong>: GET /api/logout<br>- The REST server <strong>clears the cookie, destroys the session</strong>, and optionally redirects to a login or post-logout page.</p><h3>Solution Implementation</h3><p>The sections below describe the implementation of each component of the solution. The complete source code is available on <a href="https://github.com/yzhbankov/react-node-cognito-sso">GitHub [2]</a>.</p><p>This article focuses on the <strong>implementation details</strong>, not the deployment process. Instructions for deploying the client to <strong>AWS S3</strong> and the server to <strong>AWS EC2</strong> are available <a href="https://medium.com/@yaroslavzhbankov/implementing-user-authentication-with-aws-cognito-a-complete-guide-5f8cd45679c1">in [3]</a> and <a href="https://awstip.com/bookmarks-migration-from-aws-ec2-to-aws-lambda-reduced-my-bills-by-35-times-b34d8573aab0">[4],</a> respectively.</p><h3>Microsoft Entra ID</h3><p>Follow the steps below to configure Microsoft Entra ID (formerly Azure AD) for SAML-based SSO:</p><p><strong>Step 1.</strong> Log in to the <strong>Microsoft Azure Portal</strong> and click on the <strong>Microsoft Entra ID</strong> icon, as shown in <strong>Fig. 3</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NU-_VUs2jubVnFT8gVFSZw.png" /><figcaption>Fig. 3 Microsoft Azure Portal</figcaption></figure><p><strong>Step 2.</strong> In the left-hand navigation menu, click on <strong>Enterprise applications</strong>, as shown in <strong>Fig. 4</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*687tqHOeDqcInEhnVUJLBg.png" /><figcaption>Fig. 4. Microsoft Entra ID page</figcaption></figure><p><strong>Step 3.</strong> Click the <strong>New application</strong> button to create a new application for the solution (see <strong>Fig. 5</strong>).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xVSyHqhCERN24_uCvO-uJw.png" /><figcaption>Fig. 5. Enterprise applications screen</figcaption></figure><p><strong>Step 4.</strong> On the next screen, click <strong>Create your own application</strong> to define a custom application (see <strong>Fig. 6</strong>).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*U8LHR_SWD5ZS_QCdDNk_Cw.png" /><figcaption>Fig. 6. Adding application page</figcaption></figure><p><strong>Step 5.</strong> In the panel on the right, choose the option: <strong>“Integrate any other application you don’t find in the gallery (Non-gallery)”</strong>, enter your desired application name, and click <strong>Create</strong> (see <strong>Fig. 7</strong>).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hxA8Hdp8zW_qLy1gCGovBg.png" /><figcaption>Fig. 7. Application creation page</figcaption></figure><p><strong>Step 6.</strong> Since we’re setting up SSO, click on the <strong>Set up single sign on</strong> option (see <strong>Fig. 8</strong>).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tfytYP2gho-06PVZqXCnBw.png" /><figcaption>Fig. 8. Application use case selection</figcaption></figure><p><strong>Step 7.</strong> Select <strong>SAML</strong> as the SSO method (see <strong>Fig. 9</strong>).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gWCZXCMfRk9w2vZa6kqeAA.png" /><figcaption>Fig. 9. Sign-on method selection</figcaption></figure><p><strong>Step 8.</strong> On the opened SAML configuration page, click the <strong>Edit</strong> button. In the panel that opens on the right, configure the following:</p><ul><li><strong>Identifier (Entity ID)</strong>: Use a unique name for your application.<br> Example: urn:yzhbankov:nodejs<br> (This will be referenced later as SAML_ISSUER)</li><li><strong>Reply URL (Assertion Consumer Service URL)</strong>: Provide the full URL to the endpoint on backend server that handles the SAML authentication response.<br> Example: http://localhost:3000/api/auth/callback<br> (This will be referenced later as SAML_CALLBACK_URL)</li></ul><p>Click <strong>Save</strong> to apply the settings (see <strong>Fig. 10</strong>).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vdf-GWcHCIEl2DhC3Cs53g.png" /><figcaption>Fig. 10. Basic SAML configuration</figcaption></figure><p><strong>Step 9.</strong> After saving the configuration, the <strong>SAML certificate</strong> and <strong>Login URL</strong> will be available. These are required for the backend server:</p><ul><li>Click <strong>Download</strong> to get the certificate file, and store it at a known path (This will be referenced later as SAML_CERT_PATH).</li><li>Copy the <strong>Login URL</strong>, which will be used as SAML_ENTRY_POINT.</li></ul><p>(See <strong>Fig. 11</strong> for reference.)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a1KGqRLtjS-05cSXXNPpJw.png" /><figcaption>Fig. 11. SAML certificate and app setup</figcaption></figure><p><strong>Step 10.</strong> As a final step, navigate to the <strong>Users and groups</strong> tab (see <strong>Fig. 12</strong>) and create a new user for testing the login flow.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tJgGck3_gf7hb7fyMhzwXg.png" /><figcaption>Fig. 12. User and group management tab</figcaption></figure><p>Once these steps are completed, Microsoft Entra ID is fully configured and ready to be used as the identity provider in SAML-based SSO solution.</p><h3>Web Server</h3><p>This section describes the implementation of a basic REST API server that includes an authentication layer. The web server is implemented in <strong>Node.js</strong> with <strong>TypeScript</strong>. The service itself is intentionally simple, providing only the minimal functionality required for the use case described earlier.</p><p><strong>Application Structure<br></strong>The directory structure is as follows:</p><pre>my-app/<br>├── apps<br>│   ├── server/<br>│   │   ├── .env<br>│   │   ├── .env.defaults<br>│   │   ├── app.ts<br>│   │   ├── config.ts<br>│   │   ├── middlewares.ts<br>│   │   ├── samlStrategy.ts<br>│   │   ├── package.json<br>│   │   └── tsconfig.json<br>│   └── client/<br>│       └── ...<br>└── README.md</pre><p><strong>Configuration</strong></p><p>Configuration is handled using the dotenv library. Default values are specified in the .env.defaults file. The config.ts file loads and provides configuration values to the application.<br>This file includes basic configuration values required for the solution to work.</p><pre># ./apps/server/.env.defaults<br>SERVER_PORT=3000<br>SAML_ENTRY_POINT=https://login.microsoftonline.com/44b02d65...6f6e646a/saml2<br>SAML_ISSUER=urn:yzhbankov:nodejs<br>SAML_CALLBACK_URL=http://localhost:3000/api/auth/callback<br>SAML_CERT_PATH=/PATH_TO_CERTIFICATE_FOLDER/NodeJs SAML App.cer<br>REDIRECT_AFTER_LOGIN_URL=http://localhost:8080/home<br>LOGOUT_REDIRECT_URL=http://localhost:8080<br>JWT_SECRET=your_secret_jwt_here<br>SESSION_SECRET=your_session_secret</pre><p><strong>Descriptions</strong>:</p><ul><li>SERVER_PORT: Port number for the server.</li><li>SAML_ENTRY_POINT: IdP login URL (Microsoft Entra ID tenant-specific).</li><li>SAML_ISSUER: Unique identifier for the application, must match the Entity ID configured in the IdP.</li><li>SAML_CALLBACK_URL: The URL the IdP will post the SAML response to after successful login.</li><li>SAML_CERT_PATH: Path to the X.509 certificate used to validate the IdP signature. Certificate was downloaded in previous section.</li><li>REDIRECT_AFTER_LOGIN_URL: Frontend route users are redirected to after login.</li><li>LOGOUT_REDIRECT_URL: Redirect target after logging out.</li><li>JWT_SECRET: Secret for signing JSON Web Tokens.</li><li>SESSION_SECRET: Secret for session signing.</li></ul><p>The config.ts file loads environment variables and defines constants used throughout the server:</p><pre>// ./apps/server/config.ts<br>import fs from &#39;fs&#39;;<br>import dotenv from &#39;dotenv&#39;;<br><br>dotenv.config();<br><br>const SERVER_PORT = process.env.SERVER_PORT || 3000;<br>const SAML_ENTRY_POINT = process.env.SAML_ENTRY_POINT || &#39;&#39;;<br>const SAML_ISSUER = process.env.SAML_ISSUER || &#39;&#39;;<br>const SAML_CALLBACK_URL = process.env.SAML_CALLBACK_URL || &#39;&#39;;<br>const SAML_CERT_PATH = process.env.SAML_CERT_PATH || &#39;&#39;;<br>const SESSION_SECRET = process.env.SESSION_SECRET || &#39;your_session_secret&#39;;<br>const LOGOUT_REDIRECT_URL = process.env.LOGOUT_REDIRECT_URL || &#39;http://localhost:8080&#39;;<br>const REDIRECT_AFTER_LOGIN_URL = process.env.REDIRECT_AFTER_LOGIN_URL || &#39;/&#39;;<br>const JWT_SECRET = process.env.JWT_SECRET || &#39;your_secret&#39;;<br><br>let SAML_CERT = &#39;&#39;;<br>if (SAML_CERT_PATH) {<br>    try {<br>        SAML_CERT = fs.readFileSync(SAML_CERT_PATH, &#39;utf-8&#39;);<br>    } catch (error) {<br>        console.error(`Failed to read SAML cert at ${SAML_CERT_PATH}:`, error);<br>    }<br>}<br><br>export const config = {<br>    SERVER_PORT,<br>    SAML_ENTRY_POINT,<br>    SAML_ISSUER,<br>    SAML_CALLBACK_URL,<br>    SAML_CERT,<br>    SESSION_SECRET,<br>    LOGOUT_REDIRECT_URL,<br>    REDIRECT_AFTER_LOGIN_URL,<br>    JWT_SECRET,<br>};</pre><blockquote><strong><em>Note:</em></strong><em> </em>In a production environment, environment variables should be validated (e.g. using zod, joi or livr).</blockquote><p><strong>Application Logic</strong></p><p>The app.ts file defines the web application and its endpoints. It includes logic for login, callback, logout, and accessing a protected resource.</p><ul><li>The /api/auth/login endpoint sends a redirect response that causes the browser to navigate to the Identity Provider (IdP) login page.</li><li>After successful login, <strong>Microsoft Entra ID</strong> redirects the user back to /api/auth/callback via a <strong>POST</strong> request, including the <strong>SAML Response</strong>.</li><li>The server validates the response using passport-saml, generates a <strong>JWT</strong>, stores it in an <strong>HTTP-only cookie</strong>, and sends it back to the client.</li><li>Once the cookie is received, the client can access protected resources by including the auth_token cookie in subsequent requests.</li></ul><blockquote><strong><em>Note: </em></strong>This implementation is intentionally simplified and stateful. The server maintains session state internally, which can limit scalability. To support horizontal scaling, consider storing session data (such as JWTs) in an external service like Redis, DynamoDB, or a shared database. This allows the application to be stateless and more scalable in production.</blockquote><pre>// ./apps/server/app.ts<br>import express from &#39;express&#39;;<br>import passport from &#39;passport&#39;;<br>import jwt from &#39;jsonwebtoken&#39;;<br>import middlewares from &#39;./middlewares&#39;;<br>import { config } from &#39;./config&#39;;<br>import &#39;./samlStrategy&#39;;<br><br>const app = express();<br><br>app.use(middlewares.cookieParser());<br>app.use(middlewares.session());<br>app.use(middlewares.passportInit());<br>app.use(middlewares.passportSession());<br>app.use(middlewares.urlencode());<br>app.use(middlewares.cors());<br><br>app.get(&#39;/api/auth/login&#39;, passport.authenticate(&#39;saml&#39;, { failureRedirect: &#39;/&#39;, failureFlash: true }));<br><br>app.get(&#39;/api/auth/logout&#39;, (req, res) =&gt; {<br>    req.logout(() =&gt; {<br>        res.clearCookie(&#39;auth_token&#39;);<br>        req.session.destroy(() =&gt; {<br>            res.redirect(config.LOGOUT_REDIRECT_URL);<br>        });<br>    });<br>});<br><br>app.post(&#39;/api/auth/callback&#39;, passport.authenticate(&#39;saml&#39;, { failureRedirect: &#39;/&#39; }),<br>    (req, res) =&gt; {<br>        const token = jwt.sign(req.user, config.JWT_SECRET, { expiresIn: &#39;1h&#39; });<br>        res.cookie(&#39;auth_token&#39;, token, { httpOnly: true });<br>        res.redirect(config.REDIRECT_AFTER_LOGIN_URL);<br>    }<br>);<br><br>app.get(&#39;/api/protected&#39;, (req, res) =&gt; {<br>    const token = req.cookies.auth_token;<br>    if (!token) return res.status(401).json({ error: &#39;Unauthorized&#39; });<br><br>    try {<br>        const user = jwt.verify(token, config.JWT_SECRET);<br>        res.json({ message: &#39;Protected content&#39;, user });<br>    } catch (err) {<br>        res.status(401).json({ error: &#39;Invalid token&#39; });<br>    }<br>});<br><br>app.listen(config.SERVER_PORT, () =&gt; {<br>    console.log(`Server running on http://localhost:${config.SERVER_PORT}`);<br>});</pre><p>Endpoints:</p><ul><li>GET /api/auth/login: Redirects to IdP login page.</li><li>POST /api/auth/callback: Handles SAML response coming from IdP.</li><li>GET /api/auth/logout: Clearing user session.</li><li>GET /api/protected: A protected resource endpoint.</li></ul><p><strong>Middlewares</strong></p><p>The middlewares.ts encapsulates middleware initialization for easier reuse and separation of concerns:</p><pre>// ./apps/server/middlewares.ts<br>import cors from &#39;cors&#39;;<br>import express from &#39;express&#39;;<br>import passport from &#39;passport&#39;;<br>import session from &#39;express-session&#39;;<br>import cookieParser from &#39;cookie-parser&#39;;<br>import {config} from &#39;./config&#39;;<br><br>export default {<br>    cors: () =&gt; cors({<br>        origin: &#39;http://localhost:8080&#39;, // allow your React frontend<br>        credentials: true,              // allow cookies (auth_token) to be sent<br>    }),<br>    urlencode: () =&gt; express.urlencoded({ extended: true }),<br>    passportSession: () =&gt; passport.session(),<br>    passportInit: () =&gt; passport.initialize(),<br>    session: () =&gt; session({<br>        secret: config.SESSION_SECRET,<br>        resave: false,<br>        saveUninitialized: true,<br>        cookie: { secure: false }, // true if HTTPS<br>    }),<br>    cookieParser: () =&gt; cookieParser(),<br>}<br></pre><p><strong>SAML Strategy</strong></p><p>The samlStrategy.ts file configures the passport-saml strategy for handling SAML authentication:</p><pre>// ./apps/server/samlStrategy.ts<br>import passport from &#39;passport&#39;;<br>import { Strategy as SamlStrategy, SamlConfig, Profile } from &#39;passport-saml&#39;;<br>import { config } from &#39;./config&#39;;<br><br>type UserProfile = {<br>    id: string;<br>    email?: string;<br>    name?: string;<br>};<br><br>passport.serializeUser((user: Express.User, done) =&gt; {<br>    done(null, user);<br>});<br>passport.deserializeUser((user: Express.User, done) =&gt; {<br>    done(null, user);<br>});<br><br>const samlOptions: SamlConfig = {<br>    entryPoint: config.SAML_ENTRY_POINT,<br>    issuer: config.SAML_ISSUER,<br>    callbackUrl: config.SAML_CALLBACK_URL,<br>    cert: config.SAML_CERT,<br>    identifierFormat: null,<br>};<br><br>const samlStrategy = new SamlStrategy(samlOptions, (profile: Profile, done) =&gt; {<br>    const user: UserProfile = {<br>        id: profile.nameID || &#39;&#39;,<br>        email: profile.email || profile[&#39;http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress&#39;] as string,<br>        name: profile[&#39;http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name&#39;] as string,<br>    };<br>    return done(null, user);<br>});<br><br>passport.use(samlStrategy);</pre><h3>Web Application</h3><p>This section describes the final component of the solution — the <strong>client application</strong>, which interacts with the REST API server. Like the server, the client is intentionally kept minimal and implements basic functionality to:</p><ul><li>Authenticate a user</li><li>Retrieve a protected resource from the server</li><li>Implement logout functionality</li></ul><p>The client application is built using <strong>Next.js</strong>, a popular React framework that supports both server-side rendering and static generation.</p><p><strong>Application Structure<br></strong>The directory structure is as follows:</p><pre>my-app/<br>├── apps<br>│   ├── server/<br>│   │   └── ...<br>│   └── client/<br>│   │   ├── pages/<br>│   │   |   ├── _app.tsx<br>│   │   |   ├── home.tsx<br>│   │   |   └── index.tsx<br>│   │   ├── .env.local<br>│   │   ├── config.tsx<br>│   │   ├── tsconfig.json<br>│   │   ├── package.json<br>│   │   └── tsconfig.json<br>└── README.md</pre><p><strong>Configuration</strong></p><p>The application is configured using environment variables, defined in the .env.local file. These variables include the URL of the backend API. In a Next.js application, <strong>public environment variables</strong> (exposed to the browser) must be <a href="https://nextjs.org/docs/pages/guides/environment-variables#bundling-environment-variables-for-the-browser">prefixed with </a><a href="https://nextjs.org/docs/pages/guides/environment-variables#bundling-environment-variables-for-the-browser">NEXT_PUBLIC_</a>.</p><p>Example .env.local:</p><pre>NEXT_PUBLIC_API_URL=http://localhost:3000</pre><p>The config.tsx file loads environment variables and exports constants used throughout the client:</p><pre>interface IConfig {<br>    serverURL: string;<br>}<br><br>export default {<br>    serverURL: process.env.NEXT_PUBLIC_API_URL || &#39;&#39;<br>} as IConfig;</pre><p><strong>Application logic</strong></p><p>The _app.tsx is a top-level React component that wraps all pages. In this example, it serves as a simple wrapper, but in a real-world application, this is where you’d typically include global styles, providers, and shared layout components.</p><pre>import type { AppProps } from &#39;next/app&#39;;<br><br>export default function App({ Component, pageProps }: AppProps) {<br>    return &lt;Component {...pageProps} /&gt;;<br>}</pre><p>The index.tsx file is a main entry point of the application, served at the root URL (/). It functions as the login page.</p><p>When the user clicks the <strong>“Login with SAML (Microsoft Entra ID)”</strong> button, they are redirected to the Identity Provider login page. After successful authentication, the user is redirected back to the client (typically /home) with a cookie containing the authentication token.</p><pre>import config from &#39;../config&#39;;<br><br>export default function LoginPage() {<br>    const handleLogin = () =&gt; {<br>        window.location.href = `${config.serverURL}/api/auth/login`;<br>    };<br><br>    return (<br>        &lt;div style={{ padding: &#39;2rem&#39;, fontFamily: &#39;sans-serif&#39; }}&gt;<br>            &lt;h1&gt;Login&lt;/h1&gt;<br>            &lt;p&gt;You will be redirected to Microsoft Entra ID to sign in.&lt;/p&gt;<br>            &lt;button<br>                onClick={handleLogin}<br>                style={{<br>                    padding: &#39;0.5rem 1rem&#39;,<br>                    fontSize: &#39;1rem&#39;,<br>                    cursor: &#39;pointer&#39;,<br>                }}<br>            &gt;<br>                Login with SAML (Microsoft Entra ID)<br>            &lt;/button&gt;<br>        &lt;/div&gt;<br>    );<br>}</pre><p>The home.tsx page displays a protected resource. When the component mounts, it sends a request to the /api/protected endpoint using the authentication cookie. If the user is authenticated, the protected data is displayed; otherwise, the user is redirected to the login page.</p><p>Logout functionality is also provided: clicking the “Logout” button redirects the user to the /api/auth/logout endpoint, which clears the session on the backend and redirects the user back to the login page.</p><pre>import { useEffect, useState } from &#39;react&#39;;<br>import config from &#39;../config&#39;;<br><br>type User = {<br>    name?: string;<br>    email: string;<br>};<br><br>export default function Home() {<br>    const [user, setUser] = useState&lt;User | null&gt;(null);<br><br>    useEffect(() =&gt; {<br>        fetch(`${config.serverURL}/api/protected`, { credentials: &#39;include&#39; })<br>            .then(res =&gt; {<br>                if (!res.ok) throw new Error(&#39;Unauthorized&#39;);<br>                return res.json();<br>            })<br>            .then(data =&gt; setUser(data.user))<br>            .catch(() =&gt; {<br>                window.location.href = `${config.serverURL}/api/auth/login`;<br>            });<br>    }, []);<br><br>    const handleLogout = () =&gt; {<br>        window.location.href = `${config.serverURL}/api/auth/logout`;<br>    };<br><br>    if (!user) return &lt;div&gt;Loading...&lt;/div&gt;;<br><br>    return (<br>        &lt;div style={{ padding: &#39;2rem&#39;, fontFamily: &#39;sans-serif&#39; }}&gt;<br>            &lt;h1&gt;Welcome, {user.name || user.email}&lt;/h1&gt;<br>            &lt;p&gt;You are authenticated!&lt;/p&gt;<br>            &lt;button onClick={handleLogout} style={{ marginTop: &#39;1rem&#39; }}&gt;<br>                Logout<br>            &lt;/button&gt;<br>        &lt;/div&gt;<br>    );<br>}</pre><h3>Conclusion</h3><p>In this article, was demonstrated a practical and modular approach to integrating Microsoft Entra ID into a client–server architecture implementing <strong>SAML</strong> based SSO. Was showed how to:</p><ul><li>Configure Microsoft Entra ID with appropriate SAML settings</li><li>Implement a minimal Node.js REST API to manage the authentication flow</li><li>Create a Next.js-based frontend to handle user login, logout and protected resource access</li></ul><p>While the current implementation serves as a solid starting point, it should be enhanced with production-ready features such as making server stateless moving user session outside the REST server, using HTTPs instead of HTTP and error handling. With these additions, the solution can evolve into a robust, secure, and maintainable authentication framework suitable for real-world deployments.</p><h3>References</h3><ol><li><a href="https://medium.com/@yaroslavzhbankov/implementing-oauth-2-0-authorization-code-flow-with-aws-cognito-for-enterprise-sso-e09d8f370981">https://medium.com/@yaroslavzhbankov/implementing-oauth-2-0-authorization-code-flow-with-aws-cognito-for-enterprise-sso-e09d8f370981</a></li><li><a href="https://github.com/yzhbankov/react-node-cognito-sso">https://github.com/yzhbankov/react-node-cognito-sso</a></li><li><a href="https://medium.com/@yaroslavzhbankov/implementing-user-authentication-with-aws-cognito-a-complete-guide-5f8cd45679c1">https://medium.com/@yaroslavzhbankov/implementing-user-authentication-with-aws-cognito-a-complete-guide-5f8cd45679c1</a></li><li><a href="https://medium.com/aws-tip/bookmarks-migration-from-aws-ec2-to-aws-lambda-reduced-my-bills-by-35-times-b34d8573aab0">https://medium.com/aws-tip/bookmarks-migration-from-aws-ec2-to-aws-lambda-reduced-my-bills-by-35-times-b34d8573aab0</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8300df860d5f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Implementing OAuth 2.0 Authorization Code Flow with AWS Cognito for Enterprise SSO]]></title>
            <link>https://medium.com/@yaroslavzhbankov/implementing-oauth-2-0-authorization-code-flow-with-aws-cognito-for-enterprise-sso-e09d8f370981?source=rss-88b741f7b069------2</link>
            <guid isPermaLink="false">https://medium.com/p/e09d8f370981</guid>
            <category><![CDATA[nodejs]]></category>
            <category><![CDATA[nextjs]]></category>
            <category><![CDATA[terraform]]></category>
            <category><![CDATA[oauth2]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Dr. Yaroslav Zhbankov]]></dc:creator>
            <pubDate>Tue, 08 Jul 2025 03:44:35 GMT</pubDate>
            <atom:updated>2025-07-08T03:44:35.559Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-gBCxt9mt_iyVJHoNpIe1w.png" /></figure><h3>Introduction</h3><p>As your product matures into an enterprise-grade solution, or as the number of user accounts grows from dozens to hundreds — or even as you begin deploying the product for multiple customers — you’ll quickly face challenges related to user account management. These include increased security risks, administrative overhead and a poor user experience, especially when a single user needs to access multiple systems within your product that each use separate user management mechanisms.</p><p>The solution to these problems is centralized user management and, at a minimum, Single Sign-On (SSO). Today, nearly every modern product supports some form of SSO. Thousands of web applications offer social login options using providers like Google, Facebook, or GitHub. Implementing SSO for web applications has become relatively easy, due to widely adopted standards like OAuth 2.0, SAML and numerous libraries that follow best practices.</p><p>However, for enterprise-focused solutions, social login is often not acceptable due to security, compliance, or control requirements. In these cases, you’ll need to use dedicated authentication services that you manage or trust, such as AWS Cognito, Azure AD (Entra ID), Keycloak, or Okta. These systems offer robust capabilities but typically require a bit more effort to configure the correct authentication flow.</p><p>In this article, will be shown how to set up SSO (excluding user management) using AWS Cognito for a server–client architecture, where multiple clients — such as desktop and web applications — can authenticate through a single backend. Will be implemented the <strong>OAuth 2.0 Authorization Code Flow</strong> with client secret exchange. As an example, a simple Node.js server and a React-based frontend will be provided. This pattern is common across many projects, and hope it proves useful for you.</p><h3>Solution overview</h3><p>As described above, the solution consists of a web application built with <strong>Next.js</strong> that provides the user interface, a web server built with <strong>Node.js (Express)</strong> that exposes a REST API for accessing resources, and <strong>AWS Cognito</strong> as the centralized identity provider. The solution implements the <strong>OAuth 2.0 Authorization Code Flow</strong>, enabling SSO.</p><p>For this example, I will introduce a few requirements that define how the solution is implemented:</p><ol><li>The system should support <strong>multiple client applications</strong> — such as different web and desktop apps — used by users to access shared server resources.</li><li>It should also support <strong>multiple web servers</strong>, each serving a different customer.</li><li>Client applications must be able to work with <strong>multiple customers (servers) simultaneously</strong>, without any hardcoded or customer-specific configuration.</li></ol><p>Given these constraints, <strong>client applications should not be aware of AWS Cognito</strong>, since each customer may use a different Cognito User Pool. Therefore, some authentication logic with IdP configuration must reside on the <strong>server side</strong> while client implementing uniform flow.</p><p>Since each server is customer-specific, it will be preconfigured with the appropriate AWS Cognito User Pool. In this model, the client applications authenticate through the server, which acts as a proxy to Cognito.</p><p>The server will implement endpoints such as:</p><ul><li>GET /api/auth/login – returns the Cognito login URL</li><li>GET /api/auth/logout – returns the Cognito logout URL</li><li>GET /api/auth/refresh – refreshes the access token</li><li>GET /api/auth/callback – exchanges the authorization code for tokens</li></ul><blockquote>Note: In this particular implementation, the PKCE flow is not used and client secret will not be store in client application but in server side.</blockquote><p>Below is a diagram of the described solution (Fig. 1).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*O7eZAobK3nZNqDQcj4bNsA.png" /><figcaption>Fig. 1 Diagram of the SSO-enabled client–server solution</figcaption></figure><p>The steps shown in the diagram illustrate the basic process of how a user is authenticated in the system.</p><p>The user visits the web application <strong>(1)</strong> and decides to log in. The application sends a request to the server <strong>(2)</strong>, which responds with a login URL containing a pre-generated state value <strong>(3)</strong>. The web application then redirects the user to the AWS Cognito Hosted UI <strong>(4)</strong>. If Cognito is connected to an external identity provider (e.g., Azure AD), it may further redirect the user to that IdP.</p><p>The user authenticates via Cognito using their credentials <strong>(5)</strong>. Cognito then redirects the user back to the client application with an authorization code and the original state value <strong>(6)</strong>. The client verifies the state to prevent <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/CSRF">CSRF</a> attacks, extracts the code, and sends it to the server via a callback request <strong>(7)</strong>.</p><p>The server exchanges the authorization code for tokens by sending a request to Cognito, including the client ID, client secret, code, and redirect URI <strong>(8)</strong>. If the verification is successful, Cognito returns a set of tokens (ID token, refresh token, and access token) <strong>(9)</strong>. The server then sends these tokens back to the client <strong>(10)</strong>.</p><p>The client securely stores the tokens (e.g., using HTTP-only cookies or session storage) and can now make authenticated requests to the server. The server validates the access token included in each request to authorize access to protected resources.</p><h4>Authentication Sequence: Step-by-Step Flow</h4><p>Below is a sequence diagram provides more detailed description of the authentication flow (Fig. 2).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*n-bifebu4JFQt62n5qaW-A.png" /><figcaption>Fig. 2 The sequence diagram describing the authentication flow</figcaption></figure><ol><li><strong>User Visits the Web App</strong><br>- Loads the app in the browser.<br>- Checks for an existing access token (e.g., from memory or secure HTTP-only cookie).<br>- If no valid token shows login screen with options (e.g., Email/Password, “Login with AWS Cognito”).</li><li><strong>User Clicks “Login with AWS Cognito”</strong><br>- Web App → REST API: GET /api/auth/login.<br>- REST API builds Cognito Hosted UI login URL with the following query parameters: client_id, redirect_uri, response_type, scope, state.<br>- REST API → Web App: Returns the generated login URL.</li><li><strong>App Redirects to Cognito Hosted UI</strong><br>- Web App: window.location.href = loginUrl</li><li><strong>User Logs In via Cognito</strong><br>- Cognito Hosted UI presents login options configured in the User Pool (e.g., Email/Password, MFA).<br>- User → Cognito: Enters credentials and submits the form.</li><li><strong>Cognito Authenticates the User</strong><br>- Validates credentials.<br>- Generates an OAuth2 authorization_code.<br>- Redirects to:<br><a href="https://app.siteplan.io/auth/callback?code=abc123&amp;state=xyz">https://mywebapp.io/auth/callback?code=abc123&amp;state=xyz</a></li><li><strong>Web App Handles Redirect</strong><br>- Detects route /auth/callback<br>- Extracts: code, state<br>- Web App → REST API: POST /api/auth/callback</li><li><strong>REST API Exchanges Code for Tokens</strong><br>- REST API → Cognito (Token Endpoint): grant_type=authorization_code, code=abc123, redirect_uri=<a href="https://app.siteplan.io/auth/callback">https://</a><a href="https://app.siteplan.io/auth/callback?code=abc123&amp;state=xyz">mywebapp</a><a href="https://app.siteplan.io/auth/callback">.io/auth/callback</a>, client_id, client_secret<br>- Cognito → REST API Response: access_token, id_token, refresh_token, expires_in, token_type.</li><li><strong>REST API Returns Tokens to Web App</strong><br>- REST API → Frontend: access_token, refresh_token.</li><li><strong>Browser App Stores Tokens and Loads UI</strong><br>- Stores tokens securely (e.g., in memory or secure cookie).<br>- Begins making authenticated API requests to load user-specific data.</li></ol><h3>Solution Implementation</h3><p>The complete solution can be deployed on AWS, utilizing Terraform to streamline the cloud deployment process. The sections below describe the implementation of each solution component. The solution itself can be found on <a href="https://github.com/yzhbankov/react-node-cognito-sso">GitHub [1]</a>. This article does not focus on how to deploy the client and server to AWS, but rather on the implementation details of each component. Instructions for deploying the client to AWS S3 and the server to EC2 can be found in articles <a href="https://medium.com/@yaroslavzhbankov/implementing-user-authentication-with-aws-cognito-a-complete-guide-5f8cd45679c1">[2]</a> and <a href="https://medium.com/aws-tip/bookmarks-migration-from-aws-ec2-to-aws-lambda-reduced-my-bills-by-35-times-b34d8573aab0">[3]</a>.</p><h4>AWS Cognito</h4><p>This section describes Terraform script deploying AWS Cognito. How to configure Terraform in your project can be find in <a href="https://medium.com/aws-tip/deploying-a-simple-web-server-using-aws-api-gateway-lambdas-and-dynamodb-with-github-actions-and-ca529d4f09a2">[3]</a>. To configure auth service, it is necessary to deploy the <strong>User Pool</strong> defined in the cognito_pool section, along with the <strong>Application Client</strong> (cognito_pool_client), which handles the authentication flow, OAuth scopes, redirect URLs, and token settings. The client section also includes instructions for generating a client secret, which is used by the REST API server to interact with Cognito, and details for implementing the <strong>OAuth 2.0 Authorization Code Flow</strong>.</p><p>In this example, the OAuth scopes and token lifetimes (for ID, access, and refresh tokens) are explicitly configured. The callback and logout URLs are set to http://localhost:8080 for local development. In a production environment, these URLs should be replaced with the actual web application addresses.</p><p>The user pool configuration also specifies password requirements, ensuring secure user credential policies.</p><p>Additionally, the Terraform script provisions a default user with the email user@example.com and the password P@ssw0rd123!, making a test user available immediately after infrastructure deployment.</p><pre>resource &quot;aws_cognito_user_pool&quot; &quot;cognito_pool&quot; {<br>  name = &quot;cognito-example-user-pool&quot;<br><br>  username_attributes      = [&quot;email&quot;]<br>  auto_verified_attributes = [&quot;email&quot;]<br><br>  password_policy {<br>    minimum_length    = 8<br>    require_uppercase = true<br>    require_lowercase = true<br>    require_numbers   = true<br>    require_symbols   = true<br>  }<br>}<br><br>resource &quot;aws_cognito_user_pool_client&quot; &quot;cognito_pool_client&quot; {<br>  name                         = &quot;cognito-client&quot;<br>  user_pool_id                 = aws_cognito_user_pool.cognito_pool.id<br>  generate_secret              = true<br>  supported_identity_providers = [&quot;COGNITO&quot;]<br><br>  allowed_oauth_flows_user_pool_client = true<br>  allowed_oauth_flows = [<br>    &quot;code&quot;<br>  ]<br>  allowed_oauth_scopes = [&quot;email&quot;, &quot;openid&quot;, &quot;phone&quot;]<br><br>  explicit_auth_flows = [<br>    &quot;ALLOW_USER_AUTH&quot;,<br>    &quot;ALLOW_USER_SRP_AUTH&quot;,<br>    &quot;ALLOW_REFRESH_TOKEN_AUTH&quot;,<br>    &quot;ALLOW_ADMIN_USER_PASSWORD_AUTH&quot;,<br>    &quot;ALLOW_USER_PASSWORD_AUTH&quot;<br>  ]<br><br>  callback_urls = [<br>    &quot;http://localhost:8080&quot;<br>  ]<br><br>  logout_urls = [<br>    &quot;http://localhost:8080/logout&quot;,<br>  ]<br><br>  token_validity_units {<br>    access_token  = &quot;hours&quot;<br>    id_token      = &quot;hours&quot;<br>    refresh_token = &quot;days&quot;<br>  }<br><br>  access_token_validity  = 1<br>  id_token_validity      = 1<br>  refresh_token_validity = 5<br><br>  enable_token_revocation       = true<br>  prevent_user_existence_errors = &quot;ENABLED&quot;<br>}<br><br>resource &quot;aws_cognito_user_pool_domain&quot; &quot;cognito_domain&quot; {<br>  domain       = &quot;user-pool-domain-yz&quot;<br>  user_pool_id = aws_cognito_user_pool.cognito_pool.id<br>}<br><br>resource &quot;null_resource&quot; &quot;cognito_dependency&quot; {<br>  depends_on = [<br>    aws_cognito_user_pool.cognito_pool,<br>    aws_cognito_user_pool_client.cognito_pool_client,<br>    aws_cognito_user_pool_domain.cognito_domain<br>  ]<br>}<br><br>resource &quot;aws_cognito_user&quot; &quot;default_user&quot; {<br>  user_pool_id = aws_cognito_user_pool.cognito_pool.id<br>  username     = &quot;user@example.com&quot;<br>  attributes = {<br>    email = &quot;user@example.com&quot;<br>  }<br><br>  force_alias_creation = false<br>  message_action       = &quot;SUPPRESS&quot; # Prevents sending a signup email<br><br>  depends_on = [<br>    aws_cognito_user_pool.cognito_pool<br>  ]<br>}<br><br>resource &quot;null_resource&quot; &quot;set_default_user_password&quot; {<br>  provisioner &quot;local-exec&quot; {<br>    command = &lt;&lt;EOT<br>      aws cognito-idp admin-set-user-password \<br>        --region ${var.AWS_REGION} \<br>        --user-pool-id ${aws_cognito_user_pool.cognito_pool.id} \<br>        --username user@example.com \<br>        --password &quot;P@ssw0rd123!&quot; \<br>        --permanent<br>    EOT<br>  }<br><br>  depends_on = [<br>    aws_cognito_user.default_user<br>  ]<br>}</pre><p>AWS Cognito is a widely used service, and comprehensive documentation and user management guides are readily available in official <a href="https://docs.aws.amazon.com/cognito/latest/developerguide/managing-users.html">[5]</a> and third-party resources <a href="https://medium.com/@yaroslavzhbankov/implementing-user-authentication-with-aws-cognito-a-complete-guide-5f8cd45679c1">[2]</a>.</p><h4>Web Server</h4><p>This section describes the implementation of a basic REST API server that includes an authentication layer. The web server is implemented in <strong>Node.js</strong> with <strong>TypeScript</strong>. The service itself is intentionally simple, providing only the minimal functionality required for the use case described earlier.</p><p><strong>Application Structure<br></strong>The directory structure is as follows:</p><pre>aws-cognito/<br>├── .github/<br>│   ├── workflows/<br>|   |   ... <br>├── apps<br>│   ├── server/<br>│   │   ├── .env<br>│   │   ├── .env.defaults<br>│   │   ├── app.ts<br>│   │   ├── config.ts<br>│   │   ├── middlewares.ts<br>│   │   ├── package.json<br>│   │   └── tsconfig.json<br>│   └── client/<br>│       └── ...<br>├── dev-ops/<br>│   └── ...<br>└── README.md</pre><p><strong>Configuration</strong></p><p>Configuration is handled using the dotenv library. Default values are specified in the .env.defaults file. The config.ts file loads and provides configuration values to the application.</p><p>.env.defaults<br>This file includes basic configuration values required for the solution to work.</p><pre>SERVER_PORT=3000<br>CLIENT_ID=&lt;PUT_YOUR_CLIENT_ID&gt;<br>CLIENT_SECRET=&lt;PUT_YOUR_SERCRET&gt;<br>COGNITO_DOMAIN=user-pool-domain-yz.auth.us-east-1.amazoncognito.com<br>COGNITO_ISSUER=https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Ed6C3tdul<br>REDIRECT_URI=http://localhost:8080/<br>LOGOUT_URI=http://localhost:8080/</pre><p><strong>Descriptions</strong>:</p><ul><li>SERVER_PORT: Port number the server listens on.</li><li>CLIENT_ID: AWS Cognito client ID (retrieved from the AWS Console).</li><li>CLIENT_SECRET: AWS Cognito client secret (retrieved from the AWS Console).</li><li>COGNITO_DOMAIN: Domain name of the Cognito user pool (retrieved from the AWS Console).</li><li>COGNITO_ISSUER: Issuer URL used for token validation (retrieved from the AWS Console).</li><li>REDIRECT_URI: URI Cognito will redirect to after successful login.</li><li>LOGOUT_URI: URI Cognito will redirect to after logout.</li></ul><p>config.ts<br>This file loads environment variables and defines constants used throughout the server:</p><pre>import dotenv from &#39;dotenv&#39;;<br><br>dotenv.config();<br><br>const SERVER_PORT = process.env.SERVER_PORT || 3000;<br>const CLIENT_ID = process.env.CLIENT_ID || &#39;&#39;;<br>const CLIENT_SECRET = process.env.CLIENT_SECRET || &#39;&#39;;<br>const COGNITO_DOMAIN = process.env.COGNITO_DOMAIN || &#39;&#39;;<br>const ISSUER = process.env.COGNITO_ISSUER || &#39;&#39;;<br>const REDIRECT_URI = process.env.REDIRECT_URI || &#39;&#39;;<br>const LOGOUT_URI = process.env.LOGOUT_URI || &#39;&#39;;<br>const STATE_SECRET = &#39;secure-random-state-secret&#39;; // need to be rnadmly generated for each request<br>const TOKEN_ENDPOINT = `https://${COGNITO_DOMAIN}/oauth2/token`;<br>const JWKS_URL = `${ISSUER}/.well-known/jwks.json`;<br><br>export default {<br>    SERVER_PORT,<br>    CLIENT_ID,<br>    CLIENT_SECRET,<br>    COGNITO_DOMAIN,<br>    REDIRECT_URI,<br>    LOGOUT_URI,<br>    STATE_SECRET,<br>    TOKEN_ENDPOINT,<br>    ISSUER,<br>    JWKS_URL<br>}</pre><blockquote>Note: In a production environment, environment variables should be validated (e.g. using zod, joi or livr).</blockquote><p><strong>Application Logic</strong></p><p>app.ts<br>This file defines the web application and its endpoints. It includes logic for login, token exchange, refresh, logout, and a protected resource.</p><p>The /auth/login endpoint constructs the login URL for AWS Cognito Hosted UI. The state is currently hardcoded but should be dynamically generated for security reasons in production.</p><p>After login, Cognito redirects the user to the specified REDIRECT_URI with a code and state in the query string. The frontend verifies the state and sends the code to /auth/callback to exchange for tokens.</p><p>Once tokens are received, the client can access protected resources by including the access token in the Authorization header.</p><pre>import express, {Request, Response} from &#39;express&#39;;<br>import cors from &#39;cors&#39;;<br>import axios from &#39;axios&#39;;<br>import qs from &#39;querystring&#39;;<br>import config from &#39;./config&#39;;<br>import {checkAccess} from &#39;./middlewares&#39;;<br><br>const app = express();<br><br>app.use(express.json());<br>app.use(cors());<br><br>app.get(&#39;/auth/login&#39;, (req: Request, res: Response): void =&gt; {<br>    const state = config.STATE_SECRET;<br>    const loginUrl = `https://${config.COGNITO_DOMAIN}/oauth2/authorize?` + qs.stringify({<br>        response_type: &#39;code&#39;,<br>        client_id: config.CLIENT_ID,<br>        redirect_uri: config.REDIRECT_URI,<br>        scope: &#39;openid profile email&#39;,<br>        state,<br>    });<br>    res.json({ loginUrl });<br>});<br><br>app.get(&#39;/auth/callback&#39;, async (req: Request, res: Response): Promise&lt;void&gt; =&gt; {<br>    const code = req.query.code as string;<br>    const state = req.query.state as string;<br><br>    if (!code) {<br>        res.status(400).send(&#39;Invalid code&#39;);<br>    }<br><br>    try {<br>        const response = await axios.post(<br>            config.TOKEN_ENDPOINT,<br>            qs.stringify({<br>                grant_type: &#39;authorization_code&#39;,<br>                code,<br>                redirect_uri: config.REDIRECT_URI,<br>                client_id: config.CLIENT_ID,<br>                client_secret: config.CLIENT_SECRET,<br>            }),<br>            { headers: { &#39;Content-Type&#39;: &#39;application/x-www-form-urlencoded&#39; } }<br>        );<br><br>        res.json(response.data);<br>    } catch (error: any) {<br>        res.status(500).json({ error: &#39;Token exchange failed&#39;, details: error.response?.data });<br>    }<br>});<br><br>app.post(&#39;/auth/refresh&#39;, async (req: Request, res: Response): Promise&lt;void&gt; =&gt; {<br>    const { refresh_token } = req.body;<br><br>    try {<br>        const response = await axios.post(<br>            config.TOKEN_ENDPOINT,<br>            qs.stringify({<br>                grant_type: &#39;refresh_token&#39;,<br>                refresh_token,<br>                client_id: config.CLIENT_ID,<br>                client_secret: config.CLIENT_SECRET,<br>            }),<br>            { headers: { &#39;Content-Type&#39;: &#39;application/x-www-form-urlencoded&#39; } }<br>        );<br><br>        res.json(response.data);<br>    } catch (error: any) {<br>        res.status(500).json({ error: &#39;Token refresh failed&#39;, details: error.response?.data });<br>    }<br>});<br><br>app.get(&#39;/auth/logout&#39;, (req: Request, res: Response): void =&gt; {<br>    const logoutUrl = `https://${config.COGNITO_DOMAIN}/logout?` + qs.stringify({<br>        client_id: config.CLIENT_ID,<br>        logout_uri: config.LOGOUT_URI,<br>    });<br>    res.json({ logoutUrl });<br>});<br><br>app.get(&#39;/api/resource&#39;, checkAccess, (req, res): void =&gt; {<br>    res.json({ msg: &quot;Resource data&quot;, ts: new Date().toISOString() });<br>});<br><br>app.listen(config.SERVER_PORT, () =&gt; console.log(`Server running on port ${config.SERVER_PORT}`));</pre><p>Endpoints:</p><ul><li>GET /auth/login: Returns login URL.</li><li>GET /auth/callback: Handles token exchange with Cognito.</li><li>POST /auth/refresh: Refreshes the token using refresh token.</li><li>GET /auth/logout: Returns the logout URL for Cognito.</li><li>GET /api/resource: A protected resource endpoint.</li></ul><p><strong>Authentication Middleware</strong></p><p>middlewares.ts</p><p>This file defines the checkAccess middleware used to protect endpoints. It validates the JWT access token using the public key from AWS Cognito.</p><pre>import {Request, Response, NextFunction} from &#39;express&#39;;<br>import jwkToPem, {JWK as JwkToPemJwk} from &#39;jwk-to-pem&#39;;<br>import jwt, {JwtHeader} from &#39;jsonwebtoken&#39;;<br>import fetch from &#39;node-fetch&#39;;<br>import config from &#39;./config&#39;;<br><br><br>type CognitoJwk = JwkToPemJwk &amp; {<br>    kid: string;<br>};<br><br>async function getPublicKeys(): Promise&lt;Record&lt;string, CognitoJwk&gt;&gt; {<br>    const res = await fetch(config.JWKS_URL);<br>    if (!res.ok) throw new Error(`Failed to fetch JWKS: ${res.status}`);<br>    const json = (await res.json()) as { keys: CognitoJwk[] };<br><br>    return json.keys.reduce((acc, key) =&gt; {<br>        acc[key.kid] = key;<br>        return acc;<br>    }, {} as Record&lt;string, CognitoJwk&gt;);<br>}<br><br>async function verifyAwsToken(token: string): Promise&lt;jwt.JwtPayload&gt; {<br>    const decoded = jwt.decode(token, { complete: true }) as { header: JwtHeader } | null;<br>    if (!decoded || !decoded.header?.kid) throw new Error(&#39;Invalid token&#39;);<br><br>    const keys = await getPublicKeys();<br>    const jwk = keys[decoded.header.kid];<br>    if (!jwk) throw new Error(&#39;Invalid token&#39;);<br><br>    const pem = jwkToPem(jwk);<br><br>    const verified = jwt.verify(token, pem, {<br>        algorithms: [&#39;RS256&#39;],<br>        issuer: config.ISSUER,<br>    });<br><br>    if (typeof verified === &#39;string&#39;) {<br>        throw new Error(&#39;Unexpected token format&#39;);<br>    }<br><br>    return verified;<br>}<br><br>export const checkAccess = async (req: Request, res: Response, next: NextFunction): Promise&lt;void&gt; =&gt; {<br>    try {<br>        const authHeader = req.headers.authorization;<br><br>        if (!authHeader?.startsWith(&#39;Bearer &#39;)) {<br>            res.status(401).json({ error: &#39;Missing or invalid Authorization header&#39; });<br>            return;<br>        }<br><br>        const token = authHeader.split(&#39; &#39;)[1];<br><br>        await verifyAwsToken(token);<br><br>        next();<br>    } catch (err: any) {<br>        console.error(&#39;Token verification error:&#39;, err?.message || err);<br>        res.status(401).json({ error: &#39;Invalid token&#39; });<br>    }<br>};<br><br></pre><p>In a production setting the JWKS (public key) should be cached to avoid fetching it on every request.</p><h4>Web Application</h4><p>This section describes the missing piece — the <strong>client application</strong> — which sends requests and interacts with the REST API server. Like the server, the application is intentionally kept minimal and implements basic functionality to:</p><ul><li>Authenticate a user</li><li>Retrieve a protected resource from the server</li><li>Implement logout functionality</li></ul><p>The client application is built using <strong>Next.js</strong>, a popular React framework.</p><p><strong>Application Structure<br></strong>The directory structure is as follows:</p><pre>aws-cognito/<br>├── .github/<br>│   ├── workflows/<br>|   |   ... <br>├── apps<br>│   ├── server/<br>│   │   └── ...<br>│   └── client/<br>│   │   ├── pages/<br>│   │   |   ├── _app.tsx<br>│   │   |   ├── home.tsx<br>│   │   |   └── index.tsx<br>│   │   ├── .env.local<br>│   │   ├── config.tsx<br>│   │   ├── tsconfig.json<br>│   │   ├── package.json<br>│   │   └── tsconfig.json<br>├── dev-ops/<br>│   └── ...<br>└── README.md</pre><p><strong>Configuration</strong></p><p>The application is configured using environment variables, defined in the .env.local file. These variables include the URL of the backend API. In a Next.js application, <strong>public environment variables</strong> (exposed to the browser) must be <a href="https://nextjs.org/docs/pages/guides/environment-variables#bundling-environment-variables-for-the-browser">prefixed with </a><a href="https://nextjs.org/docs/pages/guides/environment-variables#bundling-environment-variables-for-the-browser">NEXT_PUBLIC_</a>.</p><p>Example .env.local:</p><pre>NEXT_PUBLIC_API_URL=http://localhost:3000</pre><p>config.tsx</p><p>This file loads environment variables and defines constants used throughout the client:</p><pre>interface IConfig {<br>    serverURL: string;<br>}<br><br>export default {<br>    serverURL: process.env.NEXT_PUBLIC_API_URL || &#39;&#39;<br>} as IConfig;</pre><p><strong>Application logic</strong></p><p>_app.tsx</p><p>This file is the top-level React component that wraps all pages. In this example, it’s a simple wrapper, but in a production application, this is the place to add providers, global styles, and token/context handling logic.</p><pre>import type {AppProps} from &#39;next/app&#39;;<br><br>export default function App({ Component, pageProps }: AppProps) {<br>    return &lt;Component {...pageProps} /&gt;;<br>}</pre><p>index.tsx</p><p>This is the main entry point of the application, served at the root URL (/). It functions as the <strong>login page</strong>.</p><p>When the user clicks the “<strong>Login</strong>” button, it redirects them to the authorization URL received from the backend. After a successful login, the Cognito service redirects the user back with an authorization code. This code is captured, exchanged for tokens via the backend, and the access token is stored in sessionStorage.</p><blockquote>In a real-world application, it’s more secure to handle token storage via HTTP-only cookies.</blockquote><pre><br>import {useEffect} from &#39;react&#39;;<br>import {useRouter} from &#39;next/router&#39;;<br>import config from &#39;../config&#39;;<br><br>export default function LoginPage() {<br>    const router = useRouter();<br><br>    useEffect(() =&gt; {<br>        const code = router.query.code as string;<br>        const state = router.query.state as string;<br><br>        // logic to compare state value<br>        // ...<br><br>        if (code &amp;&amp; state) {<br>            fetch(`${config.serverURL}/auth/callback?code=${encodeURIComponent(code)}`)<br>                .then(res =&gt; res.json())<br>                .then(data =&gt; {<br>                    sessionStorage.setItem(&#39;access_token&#39;, data.access_token);<br>                    router.push(&#39;/home&#39;);<br>                })<br>                .catch(err =&gt; {<br>                    console.error(&#39;Error exchanging code:&#39;, err);<br>                });<br>        }<br>    }, [router.query]);<br><br>    const handleLogin = async () =&gt; {<br>        try {<br>            const res = await fetch(`${config.serverURL}/auth/login`);<br>            const data = await res.json();<br>            if (data.loginUrl) {<br>                window.location.href = data.loginUrl;<br>            } else {<br>                console.error(&#39;Login URL not found in response&#39;);<br>            }<br>        } catch (error) {<br>            console.error(&#39;Error fetching login URL:&#39;, error);<br>        }<br>    };<br><br>    return (<br>        &lt;div style={{ padding: 40 }}&gt;<br>            &lt;h1&gt;Login Page&lt;/h1&gt;<br>            &lt;button onClick={handleLogin}&gt;Login&lt;/button&gt;<br>        &lt;/div&gt;<br>    );<br>}</pre><p>home.tsx</p><p>This component displays a protected resource. On load, it attempts to fetch an access token from sessionStorage and use it to request a secured resource from the backend via the Authorization header.</p><p>Logout logic is also implemented: the client calls the backend to terminate the Cognito session and clears the token from sessionStorage.</p><pre>// pages/home.tsx<br><br>import {useEffect, useState} from &#39;react&#39;;<br>import {useRouter} from &#39;next/router&#39;;<br>import config from &#39;../config&#39;;<br><br>export default function HomePage() {<br>    const [token, setToken] = useState&lt;string | null&gt;(null);<br>    const [resourceData, setResourceData] = useState&lt;string | null&gt;(null);<br>    const router = useRouter();<br><br>    useEffect(() =&gt; {<br>        const accessToken = sessionStorage.getItem(&#39;access_token&#39;);<br>        if (!accessToken) {<br>            router.push(&#39;/&#39;);<br>        } else {<br>            setToken(accessToken);<br>            fetchResource(accessToken);<br>        }<br>    }, [router]);<br><br>    const fetchResource = async (accessToken: string) =&gt; {<br>        try {<br>            const res = await fetch(`${config.serverURL}/api/resource`, {<br>                headers: {<br>                    &#39;Authorization&#39;: `Bearer ${accessToken}`,<br>                },<br>            });<br><br>            if (res.ok) {<br>                const data = await res.json();<br>                setResourceData(JSON.stringify(data));<br>            } else {<br>                console.error(&#39;Failed to fetch resource&#39;);<br>            }<br>        } catch (err) {<br>            console.error(&#39;Error fetching resource:&#39;, err);<br>        }<br>    };<br><br>    const handleLogout = async () =&gt; {<br>        try {<br>            const res = await fetch(`${config.serverURL}/auth/logout`, {<br>                method: &#39;GET&#39;,<br>                headers: {<br>                    &#39;Authorization&#39;: `Bearer ${token}`,<br>                },<br>            });<br><br>            if (res.ok) {<br>                const { logoutUrl } = await res.json();<br>                sessionStorage.removeItem(&#39;access_token&#39;);<br>                window.location.href = logoutUrl; // redirect to the logout URL<br>            } else {<br>                console.error(&#39;Logout request failed&#39;);<br>            }<br>        } catch (err) {<br>            console.error(&#39;Logout error:&#39;, err);<br>        }<br>    };<br><br>    return (<br>        &lt;div style={{ padding: 40 }}&gt;<br>            &lt;h1&gt;Home Page&lt;/h1&gt;<br>            &lt;p&gt;&lt;strong&gt;Access Token:&lt;/strong&gt; {token}&lt;/p&gt;<br>            &lt;p&gt;&lt;strong&gt;Resource Data:&lt;/strong&gt; {resourceData}&lt;/p&gt;<br>            &lt;button onClick={handleLogout} style={{ marginTop: 20 }}&gt;<br>                Logout<br>            &lt;/button&gt;<br>        &lt;/div&gt;<br>    );<br>}</pre><h3>Conclusion</h3><p>In this article, was demonstrated a practical and modular approach to integrating Cognito SSO into a client–server architecture implementing <strong>OAuth 2.0 Authorization Code Flow</strong>. Was showed how to:</p><ul><li>Configure AWS Cognito with appropriate OAuth settings</li><li>Implement a minimal Node.js REST API to manage the authentication flow</li><li>Create a Next.js-based frontend to handle user login, token handling, and protected resource access</li></ul><p>While the current implementation serves as a solid starting point, it should be enhanced with production-ready features such as dynamic state generation, secure HTTP-only cookies, token encryption, JWKS caching, and error handling. With these additions, the solution can evolve into a robust, secure, and maintainable authentication framework suitable for real-world deployments.</p><h3>References</h3><ol><li><a href="https://github.com/yzhbankov/react-node-cognito-sso">https://github.com/yzhbankov/react-node-cognito-sso</a></li><li><a href="https://medium.com/@yaroslavzhbankov/implementing-user-authentication-with-aws-cognito-a-complete-guide-5f8cd45679c1">https://medium.com/@yaroslavzhbankov/implementing-user-authentication-with-aws-cognito-a-complete-guide-5f8cd45679c1</a></li><li><a href="https://medium.com/aws-tip/bookmarks-migration-from-aws-ec2-to-aws-lambda-reduced-my-bills-by-35-times-b34d8573aab0">https://medium.com/aws-tip/bookmarks-migration-from-aws-ec2-to-aws-lambda-reduced-my-bills-by-35-times-b34d8573aab0</a></li><li><a href="https://medium.com/aws-tip/deploying-a-simple-web-server-using-aws-api-gateway-lambdas-and-dynamodb-with-github-actions-and-ca529d4f09a2">https://medium.com/aws-tip/deploying-a-simple-web-server-using-aws-api-gateway-lambdas-and-dynamodb-with-github-actions-and-ca529d4f09a2</a></li><li><a href="https://docs.aws.amazon.com/cognito/latest/developerguide/managing-users.html?utm_source=chatgpt.com">https://docs.aws.amazon.com/cognito/latest/developerguide/managing-users.html?utm_source=chatgpt.com</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e09d8f370981" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Caching Essentials: Types, Strategies, and Best Practices]]></title>
            <link>https://medium.com/@yaroslavzhbankov/caching-essentials-types-strategies-and-best-practices-459493cc47d9?source=rss-88b741f7b069------2</link>
            <guid isPermaLink="false">https://medium.com/p/459493cc47d9</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[software-architecture]]></category>
            <category><![CDATA[cache]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Dr. Yaroslav Zhbankov]]></dc:creator>
            <pubDate>Wed, 02 Apr 2025 02:35:03 GMT</pubDate>
            <atom:updated>2025-04-02T02:35:03.748Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*L5hs2xXEIWfpCofPL_BUEA.png" /></figure><h3>Introduction</h3><p>Caching is a crucial technique for improving speed and performance in modern computing. By storing frequently accessed data closer to applications, caching reduces latency, eases backend load, and accelerates response times. From simple in-app caching to complex distributed systems, it plays a key role across various domains.<br>At the same time, many different caching strategies exist, each serving a specific purpose. It is important to clearly understand when to choose the right one.</p><blockquote>There are only two hard things in Computer Science: cache invalidation and naming things. — Phil Karlton</blockquote><p>This article explores different caching types, strategies, use cases, and technologies.</p><h3>What is Cache?</h3><p>A <strong>cache</strong> is a hardware or software component that stores data so that future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation or a copy of data stored elsewhere.</p><h3>Cache types</h3><h4><strong>Application-Level Caching</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M5sT9Vdt31nOBG76QLUrWg.png" /></figure><p>At the application level, caching involves storing data in a fast-access layer (typically in-memory) within or close to the application itself. This contrasts with lower-level caching (e.g., CPU caches or database query caches) or higher-level caching (e.g., CDN caches). The goal is to minimize latency, reduce load on backend systems, and speed up response times for users.<br><strong><em>In-Memory Cache</em></strong><em>:</em> stores frequently used data in RAM for fast access. Common libraries: Redis (key-value store with TTL, pub/sub), Memcached (lightweight key-value store), Guava Cache (Java in-memory caching library).<br><strong><em>Object Cache</em></strong><em>: s</em>tores frequently used objects or data structures in memory. Used in frameworks like Spring Cache (Java) or Node.js LRU cache.<br><strong><em>Session Cache</em></strong>: Stores user session data to reduce database lookups. Examples: Redis (session store for web apps); Sticky Sessions (server-side session caching).</p><h4><strong>Database Caching</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MSNB4KsYYHqihbiTWQF00w.png" /></figure><p>Database caching involves keeping copies of database query results, rows, or objects in a cache (e.g., an in-memory store like Redis or Memcached, or even the database’s own built-in cache). When an application needs that data again, it retrieves it from the cache instead of hitting the database, reducing latency and load.<br><strong><em>Query Cache</em></strong><em>:</em> Stores the results of expensive database queries. Example: MySQL Query Cache (deprecated in MySQL 8).<br><strong><em>Row-Level Cache</em></strong>: Caches individual database rows in memory. Example: Hibernate Second-Level Cache.<br><strong><em>Full-Page Cache</em></strong><em>: s</em>tores entire database-generated pages for faster retrieval. Example: WordPress Full-Page Cache.<br><strong><em>Write-Through Cache</em></strong><em>:</em> Writes data to the cache and database at the same time. Ensures consistency but adds write latency.<br><strong><em>Write-Back Cache</em></strong><em>:</em> Writes data to cache first, then asynchronously updates the database. Improves write performance but risks data loss.</p><h4><strong>Distributed Caching</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6CL_-pX2eVMXJKDFblxuyQ.png" /></figure><p>In a distributed cache, the cache itself is a separate system (e.g., Redis Cluster, Memcached, Hazelcast) that runs on multiple servers working together. Data is partitioned or replicated across these nodes, allowing applications to access it quickly, even under heavy load or in distributed architectures like microservices.<br><strong><em>Content Delivery Network (CDN) Cache</em></strong>: Stores static assets (CSS, JavaScript, images) on edge servers close to users. Example: Cloudflare, Akamai, AWS CloudFront.<br><strong><em>Distributed Key-Value Stores</em></strong>: Stores cached data across multiple servers. Examples: Redis Cluster, Amazon ElastiCache, Apache Ignite.</p><h4><strong>Web Caching</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OgZ_cqvfc8N0BJFZTzeDCg.png" /></figure><p>Web caching involves temporarily saving web data at various points between the user and the origin server — such as in the browser, a proxy server, or a content delivery network (CDN). The cached data is served when the same resource is requested again, bypassing the need to fetch it from the source.<br><strong><em>Browser Cache</em></strong><em>:</em> Stores static assets in a user’s browser. Example: Cache-Control headers in HTTP.<br><strong><em>Proxy Cache</em></strong><em>:</em> Stores HTTP responses between client and server. Example: Varnish Cache, Squid Proxy.<br><strong><em>Server-Side Cache</em></strong><em>:</em> Stores generated HTML to avoid repeated computation. Example: Nginx FastCGI Cache.</p><h4><strong>Hardware-Level Caching</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*L8-U6tvO8GQ6lFNK7-5x7g.png" /></figure><p>Hardware caches are specialized memory units (usually SRAM — Static RAM) integrated into hardware components. They store copies of data that the system predicts will be needed soon, reducing the time it takes to fetch that data from slower main memory (DRAM) or storage (e.g., HDDs, SSDs). It’s all about minimizing latency and maximizing throughput at the lowest level of the computing stack.<br><strong><em>CPU Cache</em></strong><em>:</em> Stores frequently accessed instructions in L1, L2, L3 caches.<br><strong><em>Disk Cache</em></strong>: Stores frequently accessed disk data in RAM. Example: Linux Page Cache, Windows SuperFetch.</p><h3>Cache strategies</h3><h4>Cache Population Strategies</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*embhvp7zBfpQsnxVZM9TaA.png" /></figure><p>These determine when and how data gets into the cache.<br><strong>Cache-Aside (Lazy Loading)</strong>: The application checks the cache first. On a miss, it fetches data from the source (e.g., database) and manually stores it in the cache. <strong>Pros</strong>: Simple, app controls what’s cached. <strong>Cons</strong>: Risk of stale data; cache misses slow down first requests. <strong>Use Case</strong>: App-level caching with Redis (e.g., “check Redis, if empty, query DB and set”).</p><p><strong>Write-Through</strong>: Data is written to both the cache and the backing store (e.g., database) simultaneously. <strong>Pros</strong>: Cache stays consistent with the source; no stale data. <strong>Cons</strong>: Slower writes due to dual updates. <strong>Use Case</strong>: Hardware caches (e.g., CPU L1) or systems needing strong consistency.</p><p><strong>Write-Back (Write-Behind)</strong>: Data is written to the cache first, then asynchronously synced to the backing store later. <strong>Pros</strong>: Faster writes; reduces load on the source. <strong>Cons</strong>: Risk of data loss if cache fails before sync. <strong>Use Case</strong>: Disk controllers, distributed caches with eventual consistency.</p><p><strong>Read-Through</strong>: The cache itself fetches data from the source on a miss, transparently to the app. <strong>Pros</strong>: Simplifies app logic; cache handles loading. <strong>Cons</strong>: Tighter coupling between cache and source. <strong>Use Case</strong>: Some ORMs or caching libraries (e.g., Hibernate second-level cache).</p><p><strong>Pre-Fetching (Proactive Loading)</strong>: Predictively load data into the cache before it’s requested, based on patterns or locality. <strong>Pros</strong>: Reduces misses; speeds up access. <strong>Cons</strong>: Wastes space if predictions are wrong. <strong>Use Case</strong>: CPU caches (spatial locality), CDNs pre-loading assets.</p><h4>Cache Eviction Strategies</h4><p>These decide what data to remove when the cache is full.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_7VVeLu4adIouKpDwRZnQw.png" /></figure><p><strong>Least Recently Used (LRU)</strong>: Evict the least recently accessed item. <strong>Pros</strong>: Good for workloads with temporal locality. <strong>Cons</strong>: Overhead to track usage order. <strong>Use Case</strong>: Redis, CPU caches. <strong>When to Use</strong>: When you have workloads with temporal locality, meaning recently accessed items are likely to be used again in the near future.</p><p><strong>Most Recently Used (MRU)</strong>: Evict the most recently accessed item. <strong>Pros</strong>: Useful when recent data is less likely to be reused. <strong>Cons</strong>: Rare in practice. <strong>Use Case</strong>: Niche streaming apps. <strong>When to Use</strong>: When recent data is less likely to be reused soon after access, making eviction of recent data more beneficial.</p><p><strong>Least Frequently Used (LFU)</strong>: Evict the least frequently accessed item. <strong>Pros</strong>: Prioritizes heavily used data. <strong>Cons</strong>: Needs frequency tracking; slow to adapt to changing patterns. <strong>Use Case</strong>: Web caches with stable access patterns. <strong>When to Use</strong>: When you want to prioritize data that is frequently accessed over time, and the access pattern is relatively stable.</p><p><strong>First-In, First-Out (FIFO)</strong>: Evict the oldest item, regardless of usage. <strong>Pros</strong>: Simple to implement. <strong>Cons</strong>: Ignores access patterns. <strong>Use Case</strong>: Basic queues or hardware buffers. <strong>When to Use</strong>: When you don’t care about the usage patterns of the items and just need to evict the oldest data in a predictable way.</p><p><strong>Time-to-Live (TTL)</strong>: Data expires after a set time, regardless of usage. <strong>Pros</strong>: Predictable expiration; prevents staleness. <strong>Cons</strong>: May evict useful data too soon. <strong>Use Case</strong>: Web caching (HTTP Cache-Control), Redis with expiration. <strong>When to Use</strong>: When you need data to expire after a fixed period, regardless of usage, to avoid serving stale or outdated data.</p><p><strong>Random Eviction</strong>: Evict a random item. <strong>Pros</strong>: Very simple; low overhead. <strong>Cons</strong>: Unpredictable performance. <strong>Use Case</strong>: Lightweight systems or as a fallback. <strong>When to Use</strong>: When simplicity and low overhead are more important than eviction precision, or when eviction can be done with minimal performance concerns.</p><h4>Cache Consistency Strategies</h4><p>These handle how cached data stays in sync with the source.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*c9JQDsgsnvIxYKdh0W-6Rg.png" /></figure><p><strong>Write-Invalidate</strong>: When data is updated in the source, invalidate (remove) the corresponding cache entry. <strong>Pros</strong>: Ensures fresh data on next read. <strong>Cons</strong>: Next read causes a miss. <strong>Use Case</strong>: Distributed caches (e.g., invalidate Redis key on DB update).</p><p><strong>Write-Update</strong>: Update the cache immediately when the source changes. <strong>Pros</strong>: Cache stays fresh; no misses after updates. <strong>Cons</strong>: Higher overhead to propagate updates. <strong>Use Case</strong>: Multi-core CPU cache coherence (e.g., MESI protocol).</p><p><strong>Eventual Consistency</strong>: Caching refers to a strategy where data in the cache and the underlying data source (like a database) may not be immediately synchronized, but they will become consistent over time. <strong>Pros</strong>: Scales well; tolerates delays. <strong>Cons</strong>: Temporary staleness. <strong>Use Case</strong>: Distributed systems like CDNs.</p><p><strong>Strong Consistency</strong>: Cache and source are always in sync (e.g., via write-through or locking). <strong>Pros</strong>: No stale data. <strong>Cons</strong>: Slower; less scalable. <strong>Use Case</strong>: Critical systems (e.g., financial apps).</p><h4>Cache Placement Strategies</h4><p>These determine where the cache lives.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*z-DXWOOV-4clN8EpnwYiSg.png" /></figure><p><strong>Local Caching</strong>: Cache is on the same machine or process as the app. <strong>Pros</strong>: Fastest access; no network. <strong>Cons</strong>: Limited size; not shared. <strong>Use Case</strong>: CPU caches, app-level in-memory stores.</p><p><strong>Distributed Caching</strong>: Cache spans multiple nodes in a cluster. <strong>Pros</strong>: Scalable; shared across apps. <strong>Cons</strong>: Network latency; complex. <strong>Use Case</strong>: Redis Cluster, Memcached in microservices.</p><p><strong>Edge Caching</strong>: Cache is near the user (e.g., CDN edge nodes). <strong>Pros</strong>: Reduces latency for end users. <strong>Cons</strong>: Harder to invalidate globally. <strong>Use Case</strong>: Web caching with Cloudflare.</p><h4>Specialized Strategies</h4><p>These are tailored to specific domains or needs.</p><p><strong>Opportunistic Caching</strong>: Caching strategy where data is cached when it is accessed or processed for another reason, even if caching wasn’t the primary intent. It’s a “take advantage of the opportunity” approach — whenever data is fetched, the system stores it in the cache in case it’s needed again soon. <strong>Use Case</strong>: Mobile apps caching data during syncs.</p><p><strong>Adaptive Caching</strong>: Dynamic caching strategy where the system continuously adjusts its caching policies based on real-time data access patterns, workload changes, or resource availability. Unlike static caching strategies, <strong>Adaptive Caching</strong> uses algorithms and monitoring to optimize cache size, eviction policies, or data retention dynamically. <strong>Use Case</strong>: Advanced systems like databases (PostgreSQL buffer manager).</p><p><strong>Negative Caching</strong>: Caching strategy where the system caches failed or negative responses (e.g., “not found” or “error”) instead of successfully fetched data. The idea is to avoid repeated and unnecessary attempts to fetch the same data when it’s known to be unavailable or erroneous, thereby improving performance by reducing the load on the backend or data source. <strong>Use Case</strong>: DNS caching, web proxies.</p><p><strong>Query Result Caching: </strong>When you want to cache the results of expensive or frequent database queries to avoid redundant calculations and reduce response time. <strong>Use Case</strong>: SQL databases, web applications with repetitive queries.</p><p><strong>Geospatial Caching:</strong> When you need to cache geospatial data for faster retrieval of location-based queries, especially in systems dealing with proximity searches, maps, or location-based services. <strong>Use Case</strong>: Location-based services, ride-sharing apps, geospatial search engines.</p><h3>Cache Trade-offs</h3><h4>Speed vs. Accuracy</h4><p>Caching improves data retrieval speed by storing frequently accessed data in memory. However, the cached data may become stale if not properly invalidated, leading to potential inaccuracies. <strong>Improved Speed</strong>: Cache reduces the need for repeated expensive operations (e.g., database queries, computations), leading to faster responses. <strong>Potential Inaccuracy</strong>: Data served from the cache may be outdated if not updated frequently, leading to inconsistencies.</p><h4>Memory Usage vs. Cache Size</h4><p>Caching data takes up memory, and the more data you store, the higher the memory usage. Deciding how much data to cache requires finding a balance between having enough data in the cache to provide fast responses and not consuming too much memory. <strong>Increased Cache Size</strong>: More data in the cache means faster access times for a broader range of queries. <strong>Higher Memory Overhead</strong>: Storing a large volume of data can increase memory consumption, which can impact other processes or even cause out-of-memory errors in constrained environments.</p><h4>Cache Hit Ratio vs. Eviction Policies</h4><p>Cache hit ratio (the proportion of requests served from the cache) directly impacts the effectiveness of the cache. To maintain a high hit ratio, you need to ensure that the most relevant data remains cached. Eviction policies such as <strong>LRU</strong> (Least Recently Used) or <strong>LFU</strong> (Least Frequently Used) are employed to manage cache size by evicting items when the cache becomes full. <strong>High Cache Hit Ratio</strong>: More requests are served from the cache, reducing load on the backend system, improving performance, and decreasing latency. <strong>Eviction Overhead</strong>: Frequent evictions or cache misses can degrade performance, as the system has to re-fetch or recompute data.</p><h4>Data Freshness vs. Caching Duration</h4><p>Caching data for long periods can improve performance by reducing the need to fetch the data repeatedly. However, the longer the data is cached, the greater the chance it becomes stale. <strong>Longer Caching Duration</strong>: Reduces the need to perform the same operations over and over, improving performance. <strong>Stale Data</strong>: If the data changes frequently, long caching durations can lead to serving outdated information.</p><h4>Write Performance vs. Read Performance</h4><p>While caches can drastically improve read performance, writes to the cache can become more complex, especially in systems that involve <strong>write-through</strong> or <strong>write-behind</strong> strategies. <strong>Write-Through</strong>: Every time data is written to the cache, it is also written to the underlying storage. This can slow down write operations but ensures the cache and storage are synchronized. <strong>Write-Behind</strong>: Writes to the cache are asynchronous, improving write performance, but there is a risk of data inconsistency if the write to storage fails.</p><h4>Cache Invalidation Complexity</h4><p>Invalidating the cache when data changes is critical to maintaining data consistency. However, designing an effective invalidation strategy can be complex, particularly in distributed systems or scenarios with highly dynamic data. <strong>Effective Invalidation</strong>: Ensures that data is always up-to-date, but invalidation logic can be complicated to implement and could introduce additional overhead. <strong>Invalidation Overhead</strong>: Invalidation mechanisms such as <strong>time-to-live (TTL)</strong> or <strong>manual invalidation</strong> can create performance bottlenecks if not optimized correctly.</p><h4>Simplicity vs. Sophistication of Caching Strategies</h4><p>Simple caching strategies (like <strong>FIFO</strong> or <strong>Random Eviction</strong>) are easy to implement but may not be as effective as more sophisticated ones (like <strong>LRU</strong> or <strong>LFU</strong>) in specific use cases. <strong>Simple Strategies</strong>: Easier to implement and maintain with low computational overhead, but less efficient in handling complex data access patterns. <strong>Sophisticated Strategies</strong>: Provide better performance for specific workloads (e.g., LFU for stable access patterns), but come with higher implementation complexity and overhead.</p><h4>Latency vs. Cache Update Latency</h4><p>Caching strategies often aim to reduce latency, but the process of updating the cache itself may introduce delays. <strong>Low Latency Reads</strong>: Data retrieval from the cache is fast, improving application responsiveness. <strong>Cache Update Latency</strong>: Depending on whether updates to the cache are synchronous or asynchronous, cache updates may introduce delay, especially in write-heavy applications.</p><h4>Distributed Caching vs. Local Caching</h4><p><strong>Distributed caches</strong> provide shared cache access across multiple servers, improving scalability and consistency across nodes, but they introduce network overhead and potential latency. <strong>Local caches</strong> are faster but limited to the node’s scope. <strong>Distributed Caching</strong>: Helps with horizontal scaling and consistency but may introduce network latency. <strong>Local Caching</strong>: Faster due to no network overhead, but the cache is not shared across servers, limiting scalability.</p><h3>When use cache?</h3><h4>High Latency or Slow Data Access</h4><p>Fetching data from the source (e.g., database, API, disk) takes too long. <strong>Cache </strong>reduce response time by serving data from faster memory. <strong>When</strong>: latency is a bottleneck, and users notice delays.</p><h4>Frequent Access to the Same Data</h4><p>The same data is requested repeatedly (temporal locality). <strong>Cache</strong> avoids redundant computation or retrieval. <strong>When</strong>: data has high read frequency and low change frequency.</p><h4>Expensive Computations or Queries</h4><p>Generating data requires significant CPU, memory, or I/O resources. <strong>Cache </strong>stores the result once and reuse it instead of recalculating. <strong>When</strong>: processing cost outweighs caching overhead.</p><h4>Read-Heavy Workloads</h4><p>Your system has far more reads than writes. <strong>Cache</strong> offloads the source system and speed up reads. <strong>When</strong>: read-to-write ratio is high (e.g., 10:1 or more).</p><h4>Scalability Under Load</h4><p>Traffic spikes overwhelm your backend (database, server, API). <strong>Cache</strong> reduce load on the source, allowing it to handle more requests. <strong>When</strong>: backend resources are maxed out or costly to scale.</p><h4>Network Bandwidth Constraints</h4><p>Transferring data over the network is slow or expensive. <strong>Cache</strong> stores data closer to the user or app to cut bandwidth use. <strong>When</strong>: network latency or costs impact performance.</p><h4>Temporary Data Availability</h4><p>Data is transient but reused within a short window. <strong>Cache</strong> keeps it in memory instead of regenerating or refetching. <strong>When</strong>: data has a defined lifespan and frequent access.</p><h4>Fault Tolerance or Offline Support</h4><p>Backend systems might fail or be unavailable. <strong>Cache</strong> provides a fallback to keep the system running. <strong>When</strong>: reliability or uptime is critical.</p><h3>Cache technologies</h3><h4>In-Memory Caching Technologies</h4><p><strong>Redis</strong>: Fast in-memory key-value store, often used for real-time data processing and caching. Supports persistence.<br><strong>Memcached</strong>: Simple and lightweight key-value store designed for high-performance caching.<br><strong>Hazelcast</strong>: Distributed in-memory data grid, often used for application-level caching.<br><strong>Apache Ignite</strong>: In-memory data fabric for caching and data processing.</p><h4>Database Caching Technologies</h4><p><strong>MySQL Query Cache</strong>: Caches query results to reduce database load.<br><strong>PostgreSQL Cache</strong>: Third-party extensions like pg_bouncer or pgpool-II are often used for caching.<br><strong>Oracle Database Cache</strong>: Offers in-memory database caching for accelerating read queries.</p><h4>Content Delivery Network (CDN) Caching</h4><p><strong>Cloudflare</strong>: CDN with caching capabilities for faster content delivery.<br><strong>AWS CloudFront</strong>: Caches content across global edge locations.<br><strong>Akamai</strong>: Provides caching for large-scale content delivery.<br><strong>Fastly</strong>: Edge cloud platform for real-time caching and acceleration.</p><h4>Application-Level Caching</h4><p><strong>Spring Cache</strong>: Abstraction in Spring Boot for caching method results.<br><strong>ASP.NET Cache</strong>: Built-in caching mechanism for .NET applications.<br><strong>Guava Cache</strong>: In-memory caching for Java applications.<br><strong>Caffeine Cache</strong>: High-performance Java caching library with time-based and size-based eviction policies.</p><h4>Distributed Caching Technologies</h4><p><strong>Amazon ElastiCache</strong>: Managed Redis or Memcached service.<br><strong>Azure Cache for Redis</strong>: Managed Redis cache for Azure workloads.<br><strong>Google Cloud Memorystore</strong>: Fully managed Redis and Memcached.<br><strong>NCache</strong>: .NET-based distributed caching platform.</p><h4>Browser and Client-Side Caching</h4><p><strong>Browser Cache</strong>: Caches web pages, images, and other assets locally.<br><strong>Service Workers</strong>: Client-side caching for offline functionality.<br><strong>IndexedDB</strong>: Database storage in browsers for caching data-intensive apps.</p><h4>Hybrid and Specialized Caching</h4><p><strong>Apache Cassandra</strong>: NoSQL database with built-in caching mechanisms.<br><strong>Aerospike</strong>: High-performance, real-time database with built-in caching.<br><strong>Varnish</strong>: HTTP accelerator primarily used for caching web content.<br><strong>Squid</strong>: Proxy cache for web traffic optimization.</p><h3>Conclusion</h3><p>Cache is an indispensable component in modern computing, boosting system efficiency and user satisfaction by storing frequently accessed data strategically — whether at the application level, in databases, or on hardware. From in-memory solutions like Redis and Memcached to distributed systems and CDNs, caching offers versatile techniques to reduce latency, ease network bandwidth demands, and ensure availability during traffic spikes. Effective caching hinges on balancing population, eviction, and consistency strategies to optimize speed and accuracy. In a data-driven world, understanding and applying the right caching approach distinguishes slow, resource-heavy systems from fast, reliable ones, cementing its role as a cornerstone of scalable, fault-tolerant architectures.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=459493cc47d9" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>